Replace
Seems I have to rename the filename to lowercase manually.
This commit is contained in:
parent
3f6fe0a9e7
commit
f28c9fe069
10 changed files with 3695 additions and 3364 deletions
|
@ -1,32 +1,38 @@
|
|||
import xbmc
|
||||
import xbmcgui
|
||||
import xbmcaddon
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
##################################################################################################
|
||||
|
||||
import requests
|
||||
import json
|
||||
import requests
|
||||
import logging
|
||||
|
||||
import Utils as utils
|
||||
from ClientInformation import ClientInformation
|
||||
from requests.packages.urllib3.exceptions import InsecureRequestWarning
|
||||
import xbmc
|
||||
import xbmcgui
|
||||
|
||||
import utils
|
||||
import clientinfo
|
||||
|
||||
##################################################################################################
|
||||
|
||||
# Disable requests logging
|
||||
from requests.packages.urllib3.exceptions import InsecureRequestWarning
|
||||
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
|
||||
#logging.getLogger("requests").setLevel(logging.WARNING)
|
||||
#logging.getLogger('requests').setLevel(logging.WARNING)
|
||||
|
||||
##################################################################################################
|
||||
|
||||
|
||||
class DownloadUtils():
|
||||
|
||||
# Borg - multiple instances, shared state
|
||||
_shared_state = {}
|
||||
clientInfo = ClientInformation()
|
||||
|
||||
clientInfo = clientinfo.ClientInfo()
|
||||
addonName = clientInfo.getAddonName()
|
||||
addon = xbmcaddon.Addon()
|
||||
WINDOW = xbmcgui.Window(10000)
|
||||
|
||||
# Requests session
|
||||
s = None
|
||||
timeout = 60
|
||||
timeout = 30
|
||||
|
||||
|
||||
def __init__(self):
|
||||
|
||||
|
@ -34,41 +40,44 @@ class DownloadUtils():
|
|||
|
||||
def logMsg(self, msg, lvl=1):
|
||||
|
||||
self.className = self.__class__.__name__
|
||||
utils.logMsg("%s %s" % (self.addonName, self.className), msg, int(lvl))
|
||||
className = self.__class__.__name__
|
||||
utils.logMsg("%s %s" % (self.addonName, className), msg, lvl)
|
||||
|
||||
|
||||
def setUsername(self, username):
|
||||
# Reserved for UserClient only
|
||||
# Reserved for userclient only
|
||||
self.username = username
|
||||
self.logMsg("Set username: %s" % username, 2)
|
||||
|
||||
def setUserId(self, userId):
|
||||
# Reserved for UserClient only
|
||||
# Reserved for userclient only
|
||||
self.userId = userId
|
||||
self.logMsg("Set userId: %s" % userId, 2)
|
||||
|
||||
def setServer(self, server):
|
||||
# Reserved for UserClient only
|
||||
# Reserved for userclient only
|
||||
self.server = server
|
||||
self.logMsg("Set server: %s" % server, 2)
|
||||
|
||||
def setToken(self, token):
|
||||
# Reserved for UserClient only
|
||||
# Reserved for userclient only
|
||||
self.token = token
|
||||
self.logMsg("Set token: %s" % token, 2)
|
||||
|
||||
def setSSL(self, ssl, sslclient):
|
||||
# Reserved for UserClient only
|
||||
# Reserved for userclient only
|
||||
self.sslverify = ssl
|
||||
self.sslclient = sslclient
|
||||
self.logMsg("Verify SSL host certificate: %s" % ssl, 2)
|
||||
self.logMsg("SSL client side certificate: %s" % sslclient, 2)
|
||||
|
||||
|
||||
def postCapabilities(self, deviceId):
|
||||
|
||||
# Post settings to session
|
||||
url = "{server}/mediabrowser/Sessions/Capabilities/Full"
|
||||
url = "{server}/emby/Sessions/Capabilities/Full?format=json"
|
||||
data = {
|
||||
|
||||
'PlayableMediaTypes': "Audio,Video",
|
||||
'SupportsMediaControl': True,
|
||||
'SupportedCommands': (
|
||||
|
@ -86,49 +95,57 @@ class DownloadUtils():
|
|||
}
|
||||
|
||||
self.logMsg("Capabilities URL: %s" % url, 2)
|
||||
self.logMsg("PostData: %s" % data, 2)
|
||||
self.logMsg("Postdata: %s" % data, 2)
|
||||
|
||||
try:
|
||||
self.downloadUrl(url, postBody=data, type="POST")
|
||||
self.logMsg("Posted capabilities to %s" % self.server, 1)
|
||||
except:
|
||||
self.logMsg("Posted capabilities failed.")
|
||||
self.downloadUrl(url, postBody=data, type="POST")
|
||||
self.logMsg("Posted capabilities to %s" % self.server, 2)
|
||||
|
||||
# Attempt at getting sessionId
|
||||
url = "{server}/mediabrowser/Sessions?DeviceId=%s&format=json" % deviceId
|
||||
|
||||
url = "{server}/emby/Sessions?DeviceId=%s&format=json" % deviceId
|
||||
result = self.downloadUrl(url)
|
||||
try:
|
||||
result = self.downloadUrl(url)
|
||||
self.logMsg("Session: %s" % result, 2)
|
||||
sessionId = result[0]['Id']
|
||||
|
||||
sessionId = result[0][u'Id']
|
||||
self.logMsg("SessionId: %s" % sessionId)
|
||||
self.WINDOW.setProperty("sessionId%s" % self.username, sessionId)
|
||||
except:
|
||||
except (KeyError, TypeError):
|
||||
self.logMsg("Failed to retrieve sessionId.", 1)
|
||||
else:
|
||||
# Post any permanent additional users
|
||||
additionalUsers = utils.settings('additionalUsers').split(',')
|
||||
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
|
||||
url = "{server}/mediabrowser/Users?format=json"
|
||||
result = self.downloadUrl(url)
|
||||
|
||||
if result:
|
||||
for user in result:
|
||||
username = user['Name'].lower()
|
||||
userId = user['Id']
|
||||
for additional in additionalUsers:
|
||||
addUser = additional.decode('utf-8').lower()
|
||||
else:
|
||||
self.logMsg("Session: %s" % result, 2)
|
||||
self.logMsg("SessionId: %s" % sessionId, 1)
|
||||
utils.window('emby_sessionId', value=sessionId)
|
||||
|
||||
# Post any permanent additional users
|
||||
additionalUsers = utils.settings('additionalUsers')
|
||||
if additionalUsers:
|
||||
|
||||
additionalUsers = additionalUsers.split(',')
|
||||
self.logMsg(
|
||||
"List of permanent users added to the session: %s"
|
||||
% additionalUsers, 1)
|
||||
|
||||
# Get the user list from server to get the userId
|
||||
url = "{server}/emby/Users?format=json"
|
||||
result = self.downloadUrl(url)
|
||||
|
||||
for additional in additionalUsers:
|
||||
addUser = additional.decode('utf-8').lower()
|
||||
|
||||
# Compare to server users to list of permanent additional users
|
||||
for user in result:
|
||||
username = user['Name'].lower()
|
||||
|
||||
if username in addUser:
|
||||
url = "{server}/mediabrowser/Sessions/%s/Users/%s" % (sessionId, userId)
|
||||
postdata = {}
|
||||
self.downloadUrl(url, postBody=postdata, type="POST")
|
||||
#xbmcgui.Dialog().notification("Success!", "%s added to viewing session" % username, time=1000)
|
||||
userId = user['Id']
|
||||
url = (
|
||||
"{server}/emby/Sessions/%s/Users/%s?format=json"
|
||||
% (sessionId, userId)
|
||||
)
|
||||
self.downloadUrl(url, postBody={}, type="POST")
|
||||
|
||||
|
||||
def startSession(self):
|
||||
|
||||
self.deviceId = self.clientInfo.getMachineId()
|
||||
self.deviceId = self.clientInfo.getDeviceId()
|
||||
|
||||
# User is identified from this point
|
||||
# Attach authenticated header to the session
|
||||
|
@ -152,7 +169,7 @@ class DownloadUtils():
|
|||
self.s.mount("http://", requests.adapters.HTTPAdapter(max_retries=1))
|
||||
self.s.mount("https://", requests.adapters.HTTPAdapter(max_retries=1))
|
||||
|
||||
self.logMsg("Requests session started on: %s" % self.server)
|
||||
self.logMsg("Requests session started on: %s" % self.server, 1)
|
||||
|
||||
def stopSession(self):
|
||||
try:
|
||||
|
@ -165,93 +182,116 @@ class DownloadUtils():
|
|||
clientInfo = self.clientInfo
|
||||
|
||||
deviceName = clientInfo.getDeviceName()
|
||||
deviceId = clientInfo.getMachineId()
|
||||
deviceId = clientInfo.getDeviceId()
|
||||
version = clientInfo.getVersion()
|
||||
|
||||
if not authenticate:
|
||||
# If user is not authenticated
|
||||
auth = 'MediaBrowser Client="Kodi", Device="%s", DeviceId="%s", Version="%s"' % (deviceName, deviceId, version)
|
||||
header = {'Content-type': 'application/json', 'Accept-encoding': 'gzip', 'Accept-Charset': 'UTF-8,*', 'Authorization': auth}
|
||||
auth = (
|
||||
'MediaBrowser Client="Kodi", Device="%s", DeviceId="%s", Version="%s"'
|
||||
% (deviceName, deviceId, version))
|
||||
header = {
|
||||
|
||||
'Content-type': 'application/json',
|
||||
'Accept-encoding': 'gzip',
|
||||
'Accept-Charset': 'UTF-8,*',
|
||||
'Authorization': auth
|
||||
}
|
||||
self.logMsg("Header: %s" % header, 2)
|
||||
return header
|
||||
|
||||
else:
|
||||
userId = self.userId
|
||||
token = self.token
|
||||
# Attached to the requests session
|
||||
auth = 'MediaBrowser UserId="%s", Client="Kodi", Device="%s", DeviceId="%s", Version="%s"' % (userId, deviceName, deviceId, version)
|
||||
header = {'Content-type': 'application/json', 'Accept-encoding': 'gzip', 'Accept-Charset': 'UTF-8,*', 'Authorization': auth, 'X-MediaBrowser-Token': token}
|
||||
auth = (
|
||||
'MediaBrowser UserId="%s", Client="Kodi", Device="%s", DeviceId="%s", Version="%s"'
|
||||
% (userId, deviceName, deviceId, version))
|
||||
header = {
|
||||
|
||||
'Content-type': 'application/json',
|
||||
'Accept-encoding': 'gzip',
|
||||
'Accept-Charset': 'UTF-8,*',
|
||||
'Authorization': auth,
|
||||
'X-MediaBrowser-Token': token
|
||||
}
|
||||
self.logMsg("Header: %s" % header, 2)
|
||||
return header
|
||||
|
||||
def downloadUrl(self, url, postBody=None, type="GET", authenticate=True):
|
||||
return header
|
||||
|
||||
def downloadUrl(self, url, postBody=None, type="GET", parameters=None, authenticate=True):
|
||||
|
||||
self.logMsg("=== ENTER downloadUrl ===", 2)
|
||||
|
||||
WINDOW = self.WINDOW
|
||||
timeout = self.timeout
|
||||
default_link = ""
|
||||
|
||||
try:
|
||||
|
||||
# If user is authenticated
|
||||
if (authenticate):
|
||||
# Get requests session
|
||||
try:
|
||||
s = self.s
|
||||
# Replace for the real values and append api_key
|
||||
url = url.replace("{server}", self.server, 1)
|
||||
url = url.replace("{UserId}", self.userId, 1)
|
||||
# Replace for the real values
|
||||
url = url.replace("{server}", self.server)
|
||||
url = url.replace("{UserId}", self.userId)
|
||||
|
||||
self.logMsg("URL: %s" % url, 2)
|
||||
# Prepare request
|
||||
if type == "GET":
|
||||
r = s.get(url, json=postBody, timeout=timeout)
|
||||
r = s.get(url, json=postBody, params=parameters, timeout=timeout)
|
||||
elif type == "POST":
|
||||
r = s.post(url, json=postBody, timeout=timeout)
|
||||
elif type == "DELETE":
|
||||
r = s.delete(url, json=postBody, timeout=timeout)
|
||||
|
||||
except AttributeError:
|
||||
|
||||
# request session does not exists
|
||||
# Get user information
|
||||
self.username = WINDOW.getProperty('currUser')
|
||||
self.userId = WINDOW.getProperty('userId%s' % self.username)
|
||||
self.server = WINDOW.getProperty('server%s' % self.username)
|
||||
self.token = WINDOW.getProperty('accessToken%s' % self.username)
|
||||
self.userId = utils.window('emby_currUser')
|
||||
self.server = utils.window('emby_server%s' % self.userId)
|
||||
self.token = utils.window('emby_accessToken%s' % self.userId)
|
||||
header = self.getHeader()
|
||||
verifyssl = False
|
||||
cert = None
|
||||
|
||||
# IF user enables ssl verification
|
||||
try:
|
||||
if utils.settings('sslverify') == "true":
|
||||
verifyssl = True
|
||||
if utils.settings('sslcert') != "None":
|
||||
cert = utils.settings('sslcert')
|
||||
except:
|
||||
self.logMsg("Could not load SSL settings.", 1)
|
||||
pass
|
||||
if utils.settings('sslverify') == "true":
|
||||
verifyssl = True
|
||||
if utils.settings('sslcert') != "None":
|
||||
cert = utils.settings('sslcert')
|
||||
|
||||
# Replace for the real values and append api_key
|
||||
url = url.replace("{server}", self.server, 1)
|
||||
url = url.replace("{UserId}", self.userId, 1)
|
||||
# Replace for the real values
|
||||
url = url.replace("{server}", self.server)
|
||||
url = url.replace("{UserId}", self.userId)
|
||||
|
||||
self.logMsg("URL: %s" % url, 2)
|
||||
# Prepare request
|
||||
if type == "GET":
|
||||
r = requests.get(url, json=postBody, headers=header, timeout=timeout, cert=cert, verify=verifyssl)
|
||||
r = requests.get(url,
|
||||
json=postBody,
|
||||
params=parameters,
|
||||
headers=header,
|
||||
timeout=timeout,
|
||||
cert=cert,
|
||||
verify=verifyssl)
|
||||
|
||||
elif type == "POST":
|
||||
r = requests.post(url, json=postBody, headers=header, timeout=timeout, cert=cert, verify=verifyssl)
|
||||
r = requests.post(url,
|
||||
json=postBody,
|
||||
headers=header,
|
||||
timeout=timeout,
|
||||
cert=cert,
|
||||
verify=verifyssl)
|
||||
|
||||
elif type == "DELETE":
|
||||
r = requests.delete(url, json=postBody, headers=header, timeout=timeout, cert=cert, verify=verifyssl)
|
||||
r = requests.delete(url,
|
||||
json=postBody,
|
||||
headers=header,
|
||||
timeout=timeout,
|
||||
cert=cert,
|
||||
verify=verifyssl)
|
||||
|
||||
# If user is not authenticated
|
||||
elif not authenticate:
|
||||
|
||||
self.logMsg("URL: %s" % url, 2)
|
||||
header = self.getHeader(authenticate=False)
|
||||
verifyssl = False
|
||||
|
||||
|
@ -263,41 +303,49 @@ class DownloadUtils():
|
|||
|
||||
# Prepare request
|
||||
if type == "GET":
|
||||
r = requests.get(url, json=postBody, headers=header, timeout=timeout, verify=verifyssl)
|
||||
elif type == "POST":
|
||||
r = requests.post(url, json=postBody, headers=header, timeout=timeout, verify=verifyssl)
|
||||
r = requests.get(url,
|
||||
json=postBody,
|
||||
params=parameters,
|
||||
headers=header,
|
||||
timeout=timeout,
|
||||
verify=verifyssl)
|
||||
|
||||
# Process the response
|
||||
elif type == "POST":
|
||||
r = requests.post(url,
|
||||
json=postBody,
|
||||
headers=header,
|
||||
timeout=timeout,
|
||||
verify=verifyssl)
|
||||
|
||||
##### THE RESPONSE #####
|
||||
self.logMsg(r.url, 2)
|
||||
if r.status_code == 204:
|
||||
# No body in the response
|
||||
self.logMsg("====== 204 Success ======", 2)
|
||||
return default_link
|
||||
|
||||
elif r.status_code == requests.codes.ok:
|
||||
|
||||
try:
|
||||
# UTF-8 - JSON object
|
||||
r = r.json()
|
||||
self.logMsg("====== 200 Success ======", 2)
|
||||
self.logMsg("Response: %s" % r, 2)
|
||||
return r
|
||||
|
||||
except:
|
||||
if r.headers.get('content-type') == "text/html":
|
||||
pass
|
||||
else:
|
||||
if r.headers.get('content-type') != "text/html":
|
||||
self.logMsg("Unable to convert the response for: %s" % url, 1)
|
||||
else:
|
||||
r.raise_for_status()
|
||||
|
||||
return default_link
|
||||
##### EXCEPTIONS #####
|
||||
|
||||
# TO REVIEW EXCEPTIONS
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
# Make the addon aware of status
|
||||
if WINDOW.getProperty("Server_online") != "false":
|
||||
if utils.window('emby_online') != "false":
|
||||
self.logMsg("Server unreachable at: %s" % url, 0)
|
||||
self.logMsg(e, 2)
|
||||
WINDOW.setProperty("Server_online", "false")
|
||||
pass
|
||||
utils.window('emby_online', value="false")
|
||||
|
||||
except requests.exceptions.ConnectTimeout as e:
|
||||
self.logMsg("Server timeout at: %s" % url, 0)
|
||||
|
@ -307,29 +355,35 @@ class DownloadUtils():
|
|||
|
||||
if r.status_code == 401:
|
||||
# Unauthorized
|
||||
status = WINDOW.getProperty("Server_status")
|
||||
status = utils.window('emby_serverStatus')
|
||||
|
||||
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":
|
||||
# Parental control - access restricted
|
||||
WINDOW.setProperty("Server_status", "restricted")
|
||||
xbmcgui.Dialog().notification("Emby server", "Access restricted.", xbmcgui.NOTIFICATION_ERROR, time=5000)
|
||||
utils.window('emby_serverStatus', value="restricted")
|
||||
xbmcgui.Dialog().notification(
|
||||
heading="Emby server",
|
||||
message="Access restricted.",
|
||||
icon=xbmcgui.NOTIFICATION_ERROR,
|
||||
time=5000)
|
||||
return False
|
||||
|
||||
elif r.headers['X-Application-Error-Code'] == "UnauthorizedAccessException":
|
||||
# User tried to do something his emby account doesn't allow - admin restricted in some way
|
||||
# User tried to do something his emby account doesn't allow
|
||||
pass
|
||||
|
||||
elif (status == "401") or (status == "Auth"):
|
||||
pass
|
||||
|
||||
else:
|
||||
# Tell UserClient token has been revoked.
|
||||
WINDOW.setProperty("Server_status", "401")
|
||||
elif status not in ("401", "Auth"):
|
||||
# Tell userclient token has been revoked.
|
||||
utils.window('emby_serverStatus', value="401")
|
||||
self.logMsg("HTTP Error: %s" % e, 0)
|
||||
xbmcgui.Dialog().notification("Error connecting", "Unauthorized.", xbmcgui.NOTIFICATION_ERROR)
|
||||
xbmcgui.Dialog().notification(
|
||||
heading="Error connecting",
|
||||
message="Unauthorized.",
|
||||
icon=xbmcgui.NOTIFICATION_ERROR)
|
||||
return 401
|
||||
|
||||
elif (r.status_code == 301) or (r.status_code == 302):
|
||||
elif r.status_code in (301, 302):
|
||||
# Redirects
|
||||
pass
|
||||
elif r.status_code == 400:
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,150 +1,195 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
#################################################################################################
|
||||
# Kodi Monitor
|
||||
# Watched events that occur in Kodi, like setting media watched
|
||||
#################################################################################################
|
||||
|
||||
import json
|
||||
|
||||
import xbmc
|
||||
import xbmcgui
|
||||
import xbmcaddon
|
||||
import json
|
||||
|
||||
import Utils as utils
|
||||
from WriteKodiVideoDB import WriteKodiVideoDB
|
||||
from ReadKodiDB import ReadKodiDB
|
||||
from PlayUtils import PlayUtils
|
||||
from DownloadUtils import DownloadUtils
|
||||
from PlaybackUtils import PlaybackUtils
|
||||
import clientinfo
|
||||
import downloadutils
|
||||
import embydb_functions as embydb
|
||||
import playbackutils as pbutils
|
||||
import utils
|
||||
|
||||
#################################################################################################
|
||||
|
||||
|
||||
class Kodi_Monitor( xbmc.Monitor ):
|
||||
class KodiMonitor(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):
|
||||
utils.window('kodiScan', value="true")
|
||||
self.logMsg("Kodi library scan running.", 2)
|
||||
self.logMsg("Kodi library scan %s running." % library, 2)
|
||||
if library == "video":
|
||||
utils.window('emby_kodiScan', value="true")
|
||||
|
||||
def onScanFinished(self, library):
|
||||
utils.window('kodiScan', clear=True)
|
||||
self.logMsg("Kodi library scan finished.", 2)
|
||||
self.logMsg("Kodi library scan %s finished." % library, 2)
|
||||
if library == "video":
|
||||
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
|
||||
if method not in ("Playlist.OnAdd"):
|
||||
self.logMsg("Method: %s Data: %s" % (method, data), 1)
|
||||
|
||||
jsondata = json.loads(data)
|
||||
if jsondata:
|
||||
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 data:
|
||||
data = json.loads(data)
|
||||
|
||||
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()
|
||||
if method == "Player.OnPlay":
|
||||
# Set up report progress for emby playback
|
||||
item = data.get('item')
|
||||
try:
|
||||
kodiid = item['id']
|
||||
type = item['type']
|
||||
except (KeyError, TypeError):
|
||||
self.logMsg("Properties already set for item.", 1)
|
||||
else:
|
||||
if ((utils.settings('useDirectPaths') == "1" and not type == "song") or
|
||||
(type == "song" and utils.settings('disableMusic') == "false")):
|
||||
# Set up properties for player
|
||||
embyconn = utils.kodiSQL('emby')
|
||||
embycursor = embyconn.cursor()
|
||||
emby_db = embydb.Embydb_Functions(embycursor)
|
||||
emby_dbitem = emby_db.getItem_byKodiId(kodiid, type)
|
||||
try:
|
||||
itemid = emby_dbitem[0]
|
||||
except TypeError:
|
||||
self.logMsg("No kodiid returned.", 1)
|
||||
else:
|
||||
url = "{server}/emby/Users/{UserId}/Items/%s?format=json" % itemid
|
||||
result = doUtils.downloadUrl(url)
|
||||
self.logMsg("Item: %s" % result, 2)
|
||||
|
||||
playurl = None
|
||||
count = 0
|
||||
while not playurl and count < 2:
|
||||
try:
|
||||
playurl = xbmc.Player().getPlayingFile()
|
||||
except RuntimeError:
|
||||
count += 1
|
||||
xbmc.sleep(200)
|
||||
else:
|
||||
embyid = ReadKodiDB().getEmbyIdByKodiId(id,type)
|
||||
listItem = xbmcgui.ListItem()
|
||||
playback = pbutils.PlaybackUtils(result)
|
||||
|
||||
if embyid:
|
||||
if type == "song" and utils.settings('streamMusic') == "true":
|
||||
utils.window('emby_%s.playmethod' % playurl,
|
||||
value="DirectStream")
|
||||
else:
|
||||
utils.window('emby_%s.playmethod' % playurl,
|
||||
value="DirectPlay")
|
||||
# Set properties for player.py
|
||||
playback.setProperties(playurl, listItem)
|
||||
finally:
|
||||
embycursor.close()
|
||||
|
||||
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)
|
||||
elif method == "VideoLibrary.OnUpdate":
|
||||
# Manually marking as watched/unwatched
|
||||
playcount = data.get('playcount')
|
||||
item = data.get('item')
|
||||
try:
|
||||
kodiid = item['id']
|
||||
type = item['type']
|
||||
except (KeyError, TypeError):
|
||||
self.logMsg("Item is invalid for playstate update.", 1)
|
||||
else:
|
||||
# Send notification to the server.
|
||||
embyconn = utils.kodiSQL('emby')
|
||||
embycursor = embyconn.cursor()
|
||||
emby_db = embydb.Embydb_Functions(embycursor)
|
||||
emby_dbitem = emby_db.getItem_byKodiId(kodiid, type)
|
||||
try:
|
||||
itemid = emby_dbitem[0]
|
||||
except TypeError:
|
||||
self.logMsg("Could not find itemid in emby database.", 1)
|
||||
else:
|
||||
# Stop from manually marking as watched unwatched, with actual playback.
|
||||
if utils.window('emby_skipWatched%s' % itemid) == "true":
|
||||
# property is set in player.py
|
||||
utils.window('emby_skipWatched%s' % itemid, clear=True)
|
||||
else:
|
||||
# notify the server
|
||||
url = "{server}/emby/Users/{UserId}/PlayedItems/%s?format=json" % itemid
|
||||
if playcount != 0:
|
||||
doUtils.downloadUrl(url, type="POST")
|
||||
self.logMsg("Mark as watched for itemid: %s" % itemid, 1)
|
||||
else:
|
||||
doUtils.downloadUrl(url, type="DELETE")
|
||||
self.logMsg("Mark as unwatched for itemid: %s" % itemid, 1)
|
||||
finally:
|
||||
embycursor.close()
|
||||
|
||||
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)
|
||||
elif method == "VideoLibrary.OnRemove":
|
||||
|
||||
try:
|
||||
playcount = jsondata.get('playcount')
|
||||
item = jsondata['item']['id']
|
||||
type = jsondata['item']['type']
|
||||
prop = utils.window('Played%s%s' % (type, item))
|
||||
except:
|
||||
self.logMsg("Could not process VideoLibrary.OnUpdate data.", 1)
|
||||
kodiid = data['id']
|
||||
type = data['type']
|
||||
except (KeyError, TypeError):
|
||||
self.logMsg("Item is invalid for emby deletion.", 1)
|
||||
else:
|
||||
self.logMsg("VideoLibrary.OnUpdate: %s" % data, 2)
|
||||
if prop != "true":
|
||||
# Set property to prevent the multi triggering
|
||||
utils.window('Played%s%s' % (type, item), "true")
|
||||
WriteKodiVideoDB().updatePlayCountFromKodi(item, type, playcount)
|
||||
# Send the delete action to the server.
|
||||
offerDelete = False
|
||||
|
||||
self.clearProperty(type, item)
|
||||
if type == "episode" and utils.settings('deleteTV') == "true":
|
||||
offerDelete = True
|
||||
elif type == "movie" and utils.settings('deleteMovies') == "true":
|
||||
offerDelete = True
|
||||
|
||||
if method == "System.OnWake":
|
||||
xbmc.sleep(10000) #Allow network to wake up
|
||||
WINDOW.setProperty("OnWakeSync", "true")
|
||||
if utils.settings('offerDelete') != "true":
|
||||
# Delete could be disabled, even if the subsetting is enabled.
|
||||
offerDelete = False
|
||||
|
||||
if method == "VideoLibrary.OnRemove":
|
||||
xbmc.log('Intercepted remove from sender: ' + sender + ' method: ' + method + ' data: ' + data)
|
||||
jsondata = json.loads(data)
|
||||
id = ReadKodiDB().getEmbyIdByKodiId(jsondata.get("id"), jsondata.get("type"))
|
||||
if id == None:
|
||||
return
|
||||
xbmc.log("Deleting Emby ID: " + id + " from database")
|
||||
connection = utils.KodiSQL()
|
||||
cursor = connection.cursor()
|
||||
cursor.execute("DELETE FROM emby WHERE emby_id = ?", (id,))
|
||||
connection.commit()
|
||||
cursor.close
|
||||
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()
|
||||
|
||||
if jsondata:
|
||||
if jsondata.get("type") == "episode" or "movie":
|
||||
url='{server}/mediabrowser/Items?Ids=' + id + '&format=json'
|
||||
#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
|
||||
result = DownloadUtils().downloadUrl(url)
|
||||
item = result.get("Items")[0]
|
||||
if data:
|
||||
return_value = xbmcgui.Dialog().yesno("Confirm Delete", "Delete file on Emby Server?")
|
||||
if return_value:
|
||||
url='{server}/mediabrowser/Items/' + id
|
||||
xbmc.log('Deleting via URL: ' + url)
|
||||
DownloadUtils().downloadUrl(url, type="DELETE")
|
||||
|
||||
elif method == "System.OnWake":
|
||||
# Allow network to wake up
|
||||
xbmc.sleep(10000)
|
||||
utils.window('emby_onWake', value="true")
|
||||
|
||||
elif method == "Playlist.OnClear":
|
||||
self.logMsg("Clear playback properties.", 2)
|
||||
utils.window('propertiesPlayback', clear=True)
|
||||
|
||||
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")
|
||||
utils.window('emby_customPlaylist', clear=True, windowid=10101)
|
||||
#xbmcgui.Window(10101).clearProperties()
|
||||
self.logMsg("Clear playlist properties.")
|
File diff suppressed because it is too large
Load diff
|
@ -6,118 +6,157 @@ import xbmc
|
|||
import xbmcgui
|
||||
import xbmcvfs
|
||||
|
||||
from ClientInformation import ClientInformation
|
||||
import Utils as utils
|
||||
import clientinfo
|
||||
import utils
|
||||
|
||||
#################################################################################################
|
||||
|
||||
|
||||
class PlayUtils():
|
||||
|
||||
clientInfo = ClientInformation()
|
||||
addonName = clientInfo.getAddonName()
|
||||
|
||||
def __init__(self, item):
|
||||
|
||||
self.item = item
|
||||
|
||||
self.clientInfo = clientinfo.ClientInfo()
|
||||
self.addonName = self.clientInfo.getAddonName()
|
||||
|
||||
self.userid = utils.window('emby_currUser')
|
||||
self.server = utils.window('emby_server%s' % self.userid)
|
||||
|
||||
def logMsg(self, msg, lvl=1):
|
||||
|
||||
className = self.__class__.__name__
|
||||
utils.logMsg("%s %s" % (self.addonName, className), msg, int(lvl))
|
||||
|
||||
def getPlayUrl(self, server, id, result):
|
||||
|
||||
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")
|
||||
|
||||
elif self.isDirectStream(result):
|
||||
# Try direct stream
|
||||
playurl = self.directStream(result, server, id)
|
||||
if playurl:
|
||||
self.logMsg("File is direct streaming.", 1)
|
||||
utils.window("%splaymethod" % playurl, value="DirectStream")
|
||||
|
||||
elif self.isTranscoding(result):
|
||||
# Try transcoding
|
||||
playurl = self.transcoding(result, server, id)
|
||||
if playurl:
|
||||
self.logMsg("File is transcoding.", 1)
|
||||
utils.window("%splaymethod" % playurl, value="Transcode")
|
||||
|
||||
else: # Error
|
||||
utils.window("playurlFalse", value="true")
|
||||
return
|
||||
|
||||
return playurl.encode('utf-8')
|
||||
self.className = self.__class__.__name__
|
||||
utils.logMsg("%s %s" % (self.addonName, self.className), msg, lvl)
|
||||
|
||||
|
||||
def isDirectPlay(self, result, dialog = False):
|
||||
# Requirements for Direct play:
|
||||
# FileSystem, Accessible path
|
||||
def getPlayUrl(self):
|
||||
|
||||
item = self.item
|
||||
playurl = None
|
||||
|
||||
if item['MediaSources'][0]['Protocol'] == "Http":
|
||||
# Only play as http
|
||||
self.logMsg("File protocol is http.", 1)
|
||||
playurl = self.httpPlay()
|
||||
utils.window('emby_%s.playmethod' % playurl, value="DirectStream")
|
||||
|
||||
elif self.isDirectPlay():
|
||||
|
||||
self.logMsg("File is direct playing.", 1)
|
||||
playurl = self.directPlay()
|
||||
playurl = playurl.encode('utf-8')
|
||||
# Set playmethod property
|
||||
utils.window('emby_%s.playmethod' % playurl, value="DirectPlay")
|
||||
|
||||
elif self.isDirectStream():
|
||||
|
||||
self.logMsg("File is direct streaming.", 1)
|
||||
playurl = self.directStream()
|
||||
# Set playmethod property
|
||||
utils.window('emby_%s.playmethod' % playurl, value="DirectStream")
|
||||
|
||||
elif self.isTranscoding():
|
||||
|
||||
self.logMsg("File is transcoding.", 1)
|
||||
playurl = self.transcoding()
|
||||
# Set playmethod property
|
||||
utils.window('emby_%s.playmethod' % playurl, value="Transcode")
|
||||
|
||||
return playurl
|
||||
|
||||
def httpPlay(self):
|
||||
# Audio, Video, Photo
|
||||
item = self.item
|
||||
server = self.server
|
||||
|
||||
itemid = item['Id']
|
||||
mediatype = item['MediaType']
|
||||
|
||||
if type == "Audio":
|
||||
playurl = "%s/emby/Audio/%s/stream" % (server, itemid)
|
||||
else:
|
||||
playurl = "%s/emby/Videos/%s/stream?static=true" % (server, itemid)
|
||||
|
||||
return playurl
|
||||
|
||||
def isDirectPlay(self):
|
||||
|
||||
item = self.item
|
||||
|
||||
# Requirement: Filesystem, Accessible path
|
||||
if utils.settings('playFromStream') == "true":
|
||||
# User forcing to play via HTTP instead of SMB
|
||||
self.logMsg("Can't direct play: Play from HTTP is enabled.", 1)
|
||||
# User forcing to play via HTTP
|
||||
self.logMsg("Can't direct play, play from HTTP enabled.", 1)
|
||||
return False
|
||||
|
||||
# Avoid H265 1080p
|
||||
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)
|
||||
return False
|
||||
|
||||
canDirectPlay = result['MediaSources'][0]['SupportsDirectPlay']
|
||||
# Make sure it's supported by server
|
||||
canDirectPlay = item['MediaSources'][0]['SupportsDirectPlay']
|
||||
# Make sure direct play is supported by the server
|
||||
if not canDirectPlay:
|
||||
self.logMsg("Can't direct play: Server does not allow or support it.", 1)
|
||||
self.logMsg("Can't direct play, server doesn't allow/support it.", 1)
|
||||
return False
|
||||
|
||||
location = result['LocationType']
|
||||
# File needs to be "FileSystem"
|
||||
if 'FileSystem' in location:
|
||||
# Verify if path is accessible
|
||||
if self.fileExists(result):
|
||||
return True
|
||||
else:
|
||||
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)
|
||||
if dialog:
|
||||
|
||||
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)
|
||||
location = item['LocationType']
|
||||
if location == "FileSystem":
|
||||
# Verify the path
|
||||
if not self.fileExists():
|
||||
self.logMsg("Unable to direct play.")
|
||||
try:
|
||||
count = int(utils.settings('failCount'))
|
||||
except ValueError:
|
||||
count = 0
|
||||
self.logMsg("Direct play failed: %s times." % count, 1)
|
||||
|
||||
if count < 2:
|
||||
# Let the user know that direct play failed
|
||||
utils.settings('failCount', value=str(count+1))
|
||||
xbmcgui.Dialog().notification(
|
||||
heading="Emby server",
|
||||
message="Unable to direct play.",
|
||||
icon="special://home/addons/plugin.video.emby/icon.png",
|
||||
sound=False)
|
||||
elif utils.settings('playFromStream') != "true":
|
||||
# Permanently set direct stream as true
|
||||
utils.settings('playFromStream', value="true")
|
||||
utils.settings('failCount', value="0")
|
||||
xbmcgui.Dialog().notification(
|
||||
heading="Emby server",
|
||||
message=("Direct play failed 3 times. Enabled play "
|
||||
"from HTTP in the add-on settings."),
|
||||
icon="special://home/addons/plugin.video.emby/icon.png",
|
||||
sound=False)
|
||||
return False
|
||||
|
||||
def directPlay(self, result):
|
||||
return True
|
||||
|
||||
def directPlay(self):
|
||||
|
||||
item = self.item
|
||||
|
||||
try:
|
||||
playurl = result['MediaSources'][0]['Path']
|
||||
except KeyError:
|
||||
playurl = result['Path']
|
||||
playurl = item['MediaSources'][0]['Path']
|
||||
except (IndexError, KeyError):
|
||||
playurl = item['Path']
|
||||
|
||||
if 'VideoType' in result:
|
||||
if item.get('VideoType'):
|
||||
# Specific format modification
|
||||
if 'Dvd' in result['VideoType']:
|
||||
type = item['VideoType']
|
||||
|
||||
if type == "Dvd":
|
||||
playurl = "%s/VIDEO_TS/VIDEO_TS.IFO" % playurl
|
||||
elif 'BluRay' in result['VideoType']:
|
||||
elif type == "Bluray":
|
||||
playurl = "%s/BDMV/index.bdmv" % playurl
|
||||
|
||||
# Network - SMB protocol
|
||||
if "\\\\" in playurl:
|
||||
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://")
|
||||
# Assign network protocol
|
||||
if playurl.startswith('\\\\'):
|
||||
playurl = playurl.replace("\\\\", "smb://")
|
||||
playurl = playurl.replace("\\", "/")
|
||||
|
||||
if "apple.com" in playurl:
|
||||
|
@ -126,107 +165,120 @@ class PlayUtils():
|
|||
|
||||
return playurl
|
||||
|
||||
def fileExists(self):
|
||||
|
||||
def isDirectStream(self, result):
|
||||
# Requirements for Direct stream:
|
||||
# FileSystem or Remote, BitRate, supported encoding
|
||||
if 'Path' not in self.item:
|
||||
# File has no path defined in server
|
||||
return False
|
||||
|
||||
# Convert path to direct play
|
||||
path = self.directPlay()
|
||||
self.logMsg("Verifying path: %s" % path, 1)
|
||||
|
||||
if xbmcvfs.exists(path):
|
||||
self.logMsg("Path exists.", 1)
|
||||
return True
|
||||
|
||||
elif ":" not in path:
|
||||
self.logMsg("Can't verify path, assumed linux. Still try to direct play.", 1)
|
||||
return True
|
||||
|
||||
else:
|
||||
self.logMsg("Failed to find file.")
|
||||
return False
|
||||
|
||||
def isDirectStream(self):
|
||||
|
||||
item = self.item
|
||||
|
||||
# Avoid H265 1080p
|
||||
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)
|
||||
return False
|
||||
|
||||
canDirectStream = result['MediaSources'][0]['SupportsDirectStream']
|
||||
# Make sure it's supported by server
|
||||
# Requirement: BitRate, supported encoding
|
||||
canDirectStream = item['MediaSources'][0]['SupportsDirectStream']
|
||||
# Make sure the server supports it
|
||||
if not canDirectStream:
|
||||
return False
|
||||
|
||||
location = result['LocationType']
|
||||
# File can be FileSystem or Remote, not Virtual
|
||||
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)
|
||||
# Verify the bitrate
|
||||
if not self.isNetworkSufficient():
|
||||
self.logMsg("The network speed is insufficient to direct stream file.", 1)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def directStream(self, 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
|
||||
playurl = self.directPlay(result)
|
||||
return playurl
|
||||
|
||||
if "ThemeVideo" in type:
|
||||
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)
|
||||
playurl = self.directPlay()
|
||||
elif type == "Audio":
|
||||
playurl = "%s/emby/Audio/%s/stream.mp3" % (server, itemid)
|
||||
else:
|
||||
playurl = "%s/emby/Videos/%s/stream?static=true" % (server, itemid)
|
||||
|
||||
return playurl
|
||||
|
||||
def isNetworkSufficient(self):
|
||||
|
||||
def isTranscoding(self, result):
|
||||
# Last resort, no requirements
|
||||
# BitRate
|
||||
canTranscode = result['MediaSources'][0]['SupportsTranscoding']
|
||||
# Make sure it's supported by server
|
||||
if not canTranscode:
|
||||
return False
|
||||
|
||||
location = result['LocationType']
|
||||
# File can be FileSystem or Remote, not Virtual
|
||||
if 'Virtual' in location:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def transcoding(self, result, server, id):
|
||||
|
||||
if result['Path'].endswith('.strm'):
|
||||
# Allow strm loading when transcoding
|
||||
playurl = self.directPlay(result)
|
||||
return playurl
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
def isNetworkQualitySufficient(self, result):
|
||||
# Works out if the network quality can play directly or if transcoding is needed
|
||||
settingsVideoBitRate = self.getVideoBitRate()
|
||||
settingsVideoBitRate = settingsVideoBitRate * 1000
|
||||
settings = self.getBitrate()*1000
|
||||
|
||||
try:
|
||||
mediaSources = result['MediaSources']
|
||||
sourceBitRate = int(mediaSources[0]['Bitrate'])
|
||||
except KeyError:
|
||||
sourceBitrate = int(self.item['MediaSources'][0]['Bitrate'])
|
||||
except (KeyError, TypeError):
|
||||
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:
|
||||
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 getVideoBitRate(self):
|
||||
def isTranscoding(self):
|
||||
|
||||
item = self.item
|
||||
|
||||
canTranscode = item['MediaSources'][0]['SupportsTranscoding']
|
||||
# Make sure the server supports it
|
||||
if not canTranscode:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def transcoding(self):
|
||||
|
||||
item = self.item
|
||||
|
||||
if 'Path' in item and item['Path'].endswith('.strm'):
|
||||
# Allow strm loading when transcoding
|
||||
playurl = self.directPlay()
|
||||
else:
|
||||
itemid = item['Id']
|
||||
deviceId = self.clientInfo.getDeviceId()
|
||||
playurl = (
|
||||
"%s/emby/Videos/%s/master.m3u8?MediaSourceId=%s"
|
||||
% (self.server, itemid, itemid)
|
||||
)
|
||||
playurl = (
|
||||
"%s&VideoCodec=h264&AudioCodec=ac3&MaxAudioChannels=6&deviceId=%s&VideoBitrate=%s"
|
||||
% (playurl, deviceId, self.getBitrate()*1000))
|
||||
|
||||
return playurl
|
||||
|
||||
def getBitrate(self):
|
||||
|
||||
# get the addon video quality
|
||||
videoQuality = utils.settings('videoBitRate')
|
||||
videoQuality = utils.settings('videoBitrate')
|
||||
bitrate = {
|
||||
|
||||
'0': 664,
|
||||
|
@ -253,34 +305,7 @@ class PlayUtils():
|
|||
# max bit rate supported by server (max signed 32bit integer)
|
||||
return bitrate.get(videoQuality, 2147483)
|
||||
|
||||
def fileExists(self, result):
|
||||
|
||||
if 'Path' not in result:
|
||||
# File has no path in server
|
||||
return False
|
||||
|
||||
# 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):
|
||||
def audioSubsPref(self, url):
|
||||
# For transcoding only
|
||||
# Present the list of audio to select from
|
||||
audioStreamsList = {}
|
||||
|
@ -292,15 +317,21 @@ class PlayUtils():
|
|||
selectSubsIndex = ""
|
||||
playurlprefs = "%s" % url
|
||||
|
||||
mediaStream = mediaSources[0].get('MediaStreams')
|
||||
for stream in mediaStream:
|
||||
item = self.item
|
||||
try:
|
||||
mediasources = item['MediaSources'][0]
|
||||
mediastreams = mediasources['MediaStreams']
|
||||
except (TypeError, KeyError, IndexError):
|
||||
return
|
||||
|
||||
for stream in mediastreams:
|
||||
# Since Emby returns all possible tracks together, have to sort them.
|
||||
index = stream['Index']
|
||||
type = stream['Type']
|
||||
|
||||
if 'Audio' in type:
|
||||
codec = stream['Codec']
|
||||
channelLayout = stream['ChannelLayout']
|
||||
channelLayout = stream.get('ChannelLayout', "")
|
||||
|
||||
try:
|
||||
track = "%s - %s - %s %s" % (index, stream['Language'], codec, channelLayout)
|
||||
|
@ -312,6 +343,8 @@ class PlayUtils():
|
|||
audioStreams.append(track)
|
||||
|
||||
elif 'Subtitle' in type:
|
||||
if stream['IsExternal']:
|
||||
continue
|
||||
try:
|
||||
track = "%s - %s" % (index, stream['Language'])
|
||||
except:
|
||||
|
@ -336,7 +369,7 @@ class PlayUtils():
|
|||
selectAudioIndex = audioStreamsList[selected]
|
||||
playurlprefs += "&AudioStreamIndex=%s" % selectAudioIndex
|
||||
else: # User backed out of selection
|
||||
playurlprefs += "&AudioStreamIndex=%s" % mediaSources[0]['DefaultAudioStreamIndex']
|
||||
playurlprefs += "&AudioStreamIndex=%s" % mediasources['DefaultAudioStreamIndex']
|
||||
else: # There's only one audiotrack.
|
||||
selectAudioIndex = audioStreamsList[audioStreams[0]]
|
||||
playurlprefs += "&AudioStreamIndex=%s" % selectAudioIndex
|
||||
|
@ -352,7 +385,7 @@ class PlayUtils():
|
|||
selectSubsIndex = subtitleStreamsList[selected]
|
||||
playurlprefs += "&SubtitleStreamIndex=%s" % selectSubsIndex
|
||||
else: # User backed out of selection
|
||||
playurlprefs += "&SubtitleStreamIndex=%s" % mediaSources[0].get('DefaultSubtitleStreamIndex', "")
|
||||
playurlprefs += "&SubtitleStreamIndex=%s" % mediasources.get('DefaultSubtitleStreamIndex', "")
|
||||
|
||||
# Get number of channels for selected audio track
|
||||
audioChannels = audioStreamsChannelsList.get(selectAudioIndex, 0)
|
||||
|
|
|
@ -2,71 +2,69 @@
|
|||
|
||||
#################################################################################################
|
||||
|
||||
import datetime
|
||||
import json as json
|
||||
import json
|
||||
import sys
|
||||
|
||||
import xbmc
|
||||
import xbmcaddon
|
||||
import xbmcplugin
|
||||
import xbmcgui
|
||||
import xbmcplugin
|
||||
|
||||
from API import API
|
||||
from DownloadUtils import DownloadUtils
|
||||
from PlayUtils import PlayUtils
|
||||
from ClientInformation import ClientInformation
|
||||
import Utils as utils
|
||||
import api
|
||||
import artwork
|
||||
import clientinfo
|
||||
import downloadutils
|
||||
import playutils as putils
|
||||
import playlist
|
||||
import read_embyserver as embyserver
|
||||
import utils
|
||||
|
||||
#################################################################################################
|
||||
|
||||
|
||||
class PlaybackUtils():
|
||||
|
||||
clientInfo = ClientInformation()
|
||||
doUtils = DownloadUtils()
|
||||
api = API()
|
||||
|
||||
addon = xbmcaddon.Addon()
|
||||
language = addon.getLocalizedString
|
||||
addonName = clientInfo.getAddonName()
|
||||
def __init__(self, item):
|
||||
|
||||
self.item = item
|
||||
self.API = api.API(self.item)
|
||||
|
||||
self.clientInfo = clientinfo.ClientInfo()
|
||||
self.addonName = self.clientInfo.getAddonName()
|
||||
self.doUtils = downloadutils.DownloadUtils()
|
||||
|
||||
self.userid = utils.window('emby_currUser')
|
||||
self.server = utils.window('emby_server%s' % self.userid)
|
||||
|
||||
self.artwork = artwork.Artwork()
|
||||
self.emby = embyserver.Read_EmbyServer()
|
||||
self.pl = playlist.Playlist()
|
||||
|
||||
def logMsg(self, msg, lvl=1):
|
||||
|
||||
className = self.__class__.__name__
|
||||
utils.logMsg("%s %s" % (self.addonName, className), msg, int(lvl))
|
||||
self.className = self.__class__.__name__
|
||||
utils.logMsg("%s %s" % (self.addonName, self.className), msg, lvl)
|
||||
|
||||
def PLAY(self, result, setup = "service"):
|
||||
|
||||
self.logMsg("PLAY Called", 1)
|
||||
def play(self, itemid, dbid=None):
|
||||
|
||||
self.logMsg("Play called.", 1)
|
||||
|
||||
api = self.api
|
||||
doUtils = self.doUtils
|
||||
username = utils.window('currUser')
|
||||
server = utils.window('server%s' % username)
|
||||
item = self.item
|
||||
API = self.API
|
||||
listitem = xbmcgui.ListItem()
|
||||
playutils = putils.PlayUtils(item)
|
||||
|
||||
id = result['Id']
|
||||
userdata = result['UserData']
|
||||
# Get the playurl - direct play, direct stream or transcoding
|
||||
playurl = PlayUtils().getPlayUrl(server, id, result)
|
||||
listItem = xbmcgui.ListItem()
|
||||
|
||||
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 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)
|
||||
playurl = playutils.getPlayUrl()
|
||||
if not playurl:
|
||||
return xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, listitem)
|
||||
|
||||
if dbid is None:
|
||||
# Item is not in Kodi database
|
||||
listitem.setPath(playurl)
|
||||
self.setProperties(playurl, listitem)
|
||||
return xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem)
|
||||
|
||||
############### ORGANIZE CURRENT PLAYLIST ################
|
||||
|
||||
|
@ -74,58 +72,45 @@ class PlaybackUtils():
|
|||
playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
|
||||
startPos = max(playlist.getposition(), 0) # Can return -1
|
||||
sizePlaylist = playlist.size()
|
||||
|
||||
propertiesPlayback = utils.window('propertiesPlayback') == "true"
|
||||
introsPlaylist = 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)
|
||||
propertiesPlayback = utils.window('emby_playbackProps', windowid=10101) == "true"
|
||||
introsPlaylist = False
|
||||
dummyPlaylist = False
|
||||
|
||||
self.logMsg("Playlist start position: %s" % startPos, 1)
|
||||
self.logMsg("Playlist plugin position: %s" % currentPosition, 1)
|
||||
self.logMsg("Playlist size: %s" % sizePlaylist, 1)
|
||||
|
||||
############### RESUME POINT ################
|
||||
|
||||
# Resume point for widget only
|
||||
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)
|
||||
userdata = API.getUserData()
|
||||
seektime = API.adjustResume(userdata['Resume'])
|
||||
|
||||
# We need to ensure we add the intro and additional parts only once.
|
||||
# Otherwise we get a loop.
|
||||
if not propertiesPlayback:
|
||||
|
||||
utils.window('propertiesPlayback', value="true")
|
||||
self.logMsg("Setting up properties in playlist.")
|
||||
utils.window('emby_playbackProps', value="true", windowid=10101)
|
||||
self.logMsg("Setting up properties in playlist.", 1)
|
||||
|
||||
if (not homeScreen and not seektime and
|
||||
utils.window('emby_customPlaylist', windowid=10101) != "true"):
|
||||
|
||||
self.logMsg("Adding dummy file to playlist.", 2)
|
||||
dummyPlaylist = True
|
||||
playlist.add(playurl, listitem, index=startPos)
|
||||
# Remove the original item from playlist
|
||||
self.pl.removefromPlaylist(startPos+1)
|
||||
# Readd the original item to playlist - via jsonrpc so we have full metadata
|
||||
self.pl.insertintoPlaylist(currentPosition+1, dbid, item['Type'].lower())
|
||||
currentPosition += 1
|
||||
|
||||
############### -- CHECK FOR INTROS ################
|
||||
|
||||
if utils.settings('disableCinema') == "false" and not seekTime:
|
||||
if utils.settings('enableCinema') == "true" and not seektime:
|
||||
# if we have any play them when the movie/show is not being resumed
|
||||
url = "{server}/mediabrowser/Users/{UserId}/Items/%s/Intros?format=json&ImageTypeLimit=1&Fields=Etag" % id
|
||||
url = "{server}/emby/Users/{UserId}/Items/%s/Intros?format=json" % itemid
|
||||
intros = doUtils.downloadUrl(url)
|
||||
|
||||
if intros['TotalRecordCount'] != 0:
|
||||
|
@ -141,17 +126,15 @@ class PlaybackUtils():
|
|||
if getTrailers:
|
||||
for intro in intros['Items']:
|
||||
# The server randomly returns intros, process them.
|
||||
introId = intro['Id']
|
||||
|
||||
introPlayurl = PlayUtils().getPlayUrl(server, introId, intro)
|
||||
introListItem = xbmcgui.ListItem()
|
||||
introPlayurl = putils.PlayUtils(intro).getPlayUrl()
|
||||
self.logMsg("Adding Intro: %s" % introPlayurl, 1)
|
||||
|
||||
# Set listitem and properties for intros
|
||||
self.setProperties(introPlayurl, intro, introListItem)
|
||||
self.setListItemProps(server, introId, introListItem, intro)
|
||||
pbutils = PlaybackUtils(intro)
|
||||
pbutils.setProperties(introPlayurl, introListItem)
|
||||
|
||||
playlist.add(introPlayurl, introListItem, index=currentPosition)
|
||||
self.pl.insertintoPlaylist(currentPosition, url=introPlayurl)
|
||||
introsPlaylist = True
|
||||
currentPosition += 1
|
||||
|
||||
|
@ -159,109 +142,126 @@ class PlaybackUtils():
|
|||
############### -- ADD MAIN ITEM ONLY FOR HOMESCREEN ###############
|
||||
|
||||
if homeScreen and not sizePlaylist:
|
||||
# Extend our current playlist with the actual item to play only if there's no playlist first
|
||||
# Extend our current playlist with the actual item to play
|
||||
# only if there's no playlist first
|
||||
self.logMsg("Adding main item to playlist.", 1)
|
||||
self.setListItemProps(server, id, listItem, result)
|
||||
playlist.add(playurl, listItem, index=currentPosition)
|
||||
self.pl.addtoPlaylist(dbid, item['Type'].lower())
|
||||
|
||||
# Ensure that additional parts are played after the main item
|
||||
currentPosition += 1
|
||||
|
||||
|
||||
############### -- CHECK FOR ADDITIONAL PARTS ################
|
||||
|
||||
if result.get('PartCount'):
|
||||
if item.get('PartCount'):
|
||||
# Only add to the playlist after intros have played
|
||||
partcount = result['PartCount']
|
||||
url = "{server}/mediabrowser/Videos/%s/AdditionalParts" % id
|
||||
partcount = item['PartCount']
|
||||
url = "{server}/emby/Videos/%s/AdditionalParts?format=json" % itemid
|
||||
parts = doUtils.downloadUrl(url)
|
||||
|
||||
for part in parts['Items']:
|
||||
|
||||
partId = part['Id']
|
||||
additionalPlayurl = PlayUtils().getPlayUrl(server, partId, part)
|
||||
additionalListItem = xbmcgui.ListItem()
|
||||
additionalPlayurl = putils.PlayUtils(part).getPlayUrl()
|
||||
self.logMsg("Adding additional part: %s" % partcount, 1)
|
||||
|
||||
# Set listitem and properties for each additional parts
|
||||
self.setProperties(additionalPlayurl, part, additionalListItem)
|
||||
self.setListItemProps(server, partId, additionalListItem, part)
|
||||
pbutils = PlaybackUtils(part)
|
||||
pbutils.setProperties(additionalPlayurl, additionalListItem)
|
||||
pbutils.setArtwork(additionalListItem)
|
||||
|
||||
playlist.add(additionalPlayurl, additionalListItem, index=currentPosition)
|
||||
self.pl.verifyPlaylist()
|
||||
currentPosition += 1
|
||||
|
||||
|
||||
############### ADD DUMMY TO PLAYLIST #################
|
||||
|
||||
if (not homeScreen and introsPlaylist) or (homeScreen and sizePlaylist > 0):
|
||||
# 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
|
||||
if dummyPlaylist:
|
||||
# Added a dummy file to the playlist,
|
||||
# because the first item is going to fail automatically.
|
||||
self.logMsg("Processed as a playlist. First item is skipped.", 1)
|
||||
return xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, listitem)
|
||||
|
||||
|
||||
# We just skipped adding properties. Reset flag for next time.
|
||||
elif propertiesPlayback:
|
||||
self.logMsg("Resetting properties playback flag.", 2)
|
||||
utils.window('propertiesPlayback', clear=True)
|
||||
utils.window('emby_playbackProps', clear=True, windowid=10101)
|
||||
|
||||
#self.pl.verifyPlaylist()
|
||||
########## SETUP MAIN ITEM ##########
|
||||
|
||||
self.verifyPlaylist()
|
||||
# For transcoding only, ask for audio/subs pref
|
||||
if utils.window('emby_%s.playmethod' % playurl) == "Transcode":
|
||||
playurl = playutils.audioSubsPref(playurl)
|
||||
utils.window('emby_%s.playmethod' % playurl, value="Transcode")
|
||||
|
||||
listitem.setPath(playurl)
|
||||
self.setProperties(playurl, listitem)
|
||||
|
||||
############### PLAYBACK ################
|
||||
|
||||
if not homeScreen and not introsPlaylist:
|
||||
if homeScreen and seektime:
|
||||
self.logMsg("Play as a widget item.", 1)
|
||||
self.setListItem(listitem)
|
||||
xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem)
|
||||
|
||||
self.logMsg("Processed as a single item.", 1)
|
||||
xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listItem)
|
||||
|
||||
elif dummyPlaylist:
|
||||
# Added a dummy file to the playlist because the first item is going to fail automatically.
|
||||
self.logMsg("Processed as a playlist. First item is skipped.", 1)
|
||||
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:
|
||||
self.logMsg("Play as a regular item.", 1)
|
||||
xbmc.Player().play(playlist, startpos=startPos)
|
||||
xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem)
|
||||
|
||||
def setProperties(self, playurl, listitem):
|
||||
# Set all properties necessary for plugin path playback
|
||||
item = self.item
|
||||
itemid = item['Id']
|
||||
itemtype = item['Type']
|
||||
|
||||
def verifyPlaylist(self):
|
||||
embyitem = "emby_%s" % playurl
|
||||
utils.window('%s.runtime' % embyitem, value=str(item.get('RunTimeTicks')))
|
||||
utils.window('%s.type' % embyitem, value=itemtype)
|
||||
utils.window('%s.itemid' % embyitem, value=itemid)
|
||||
|
||||
playlistitems = '{"jsonrpc": "2.0", "method": "Playlist.GetItems", "params": { "playlistid": 1 }, "id": 1}'
|
||||
items = xbmc.executeJSONRPC(playlistitems)
|
||||
self.logMsg(items, 2)
|
||||
if itemtype == "Episode":
|
||||
utils.window('%s.refreshid' % embyitem, value=item.get('SeriesId'))
|
||||
else:
|
||||
utils.window('%s.refreshid' % embyitem, value=itemid)
|
||||
|
||||
def removeFromPlaylist(self, pos):
|
||||
# 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)
|
||||
|
||||
playlistremove = '{"jsonrpc": "2.0", "method": "Playlist.Remove", "params": { "playlistid": 1, "position": %d }, "id": 1}' % pos
|
||||
result = xbmc.executeJSONRPC(playlistremove)
|
||||
self.logMsg(result, 1)
|
||||
self.setArtwork(listitem)
|
||||
|
||||
def externalSubs(self, playurl):
|
||||
|
||||
def externalSubs(self, id, playurl, mediaSources):
|
||||
|
||||
username = utils.window('currUser')
|
||||
server = utils.window('server%s' % username)
|
||||
externalsubs = []
|
||||
mapping = {}
|
||||
|
||||
mediaStream = mediaSources[0].get('MediaStreams')
|
||||
item = self.item
|
||||
itemid = item['Id']
|
||||
try:
|
||||
mediastreams = item['MediaSources'][0]['MediaStreams']
|
||||
except (TypeError, KeyError, IndexError):
|
||||
return
|
||||
|
||||
kodiindex = 0
|
||||
for stream in mediaStream:
|
||||
for stream in mediastreams:
|
||||
|
||||
index = stream['Index']
|
||||
# Since Emby returns all possible tracks together, have to pull only external subtitles.
|
||||
# IsTextSubtitleStream if true, is available to download from emby.
|
||||
if "Subtitle" in stream['Type'] and stream['IsExternal'] and stream['IsTextSubtitleStream']:
|
||||
if (stream['Type'] == "Subtitle" and
|
||||
stream['IsExternal'] and stream['IsTextSubtitleStream']):
|
||||
|
||||
playmethod = utils.window("%splaymethod" % playurl)
|
||||
|
||||
if "DirectPlay" in playmethod:
|
||||
# Direct play, get direct path
|
||||
url = PlayUtils().directPlay(stream)
|
||||
elif "DirectStream" in playmethod: # Direct stream
|
||||
url = "%s/Videos/%s/%s/Subtitles/%s/Stream.srt" % (server, id, id, index)
|
||||
# Direct stream
|
||||
url = ("%s/Videos/%s/%s/Subtitles/%s/Stream.srt"
|
||||
% (self.server, itemid, itemid, index))
|
||||
|
||||
# map external subtitles for mapping
|
||||
mapping[kodiindex] = index
|
||||
|
@ -269,69 +269,79 @@ class PlaybackUtils():
|
|||
kodiindex += 1
|
||||
|
||||
mapping = json.dumps(mapping)
|
||||
utils.window('%sIndexMapping' % playurl, value=mapping)
|
||||
utils.window('emby_%s.indexMapping' % playurl, value=mapping)
|
||||
|
||||
return externalsubs
|
||||
|
||||
|
||||
def setProperties(self, playurl, result, listItem):
|
||||
# Set runtimeticks, type, refresh_id and item_id
|
||||
id = result.get('Id')
|
||||
type = result.get('Type', "")
|
||||
|
||||
utils.window("%sruntimeticks" % playurl, value=str(result.get('RunTimeTicks')))
|
||||
utils.window("%stype" % playurl, value=type)
|
||||
utils.window("%sitem_id" % playurl, value=id)
|
||||
|
||||
if type == "Episode":
|
||||
utils.window("%srefresh_id" % playurl, value=result.get('SeriesId'))
|
||||
else:
|
||||
utils.window("%srefresh_id" % playurl, value=id)
|
||||
|
||||
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)
|
||||
|
||||
def setArt(self, list, name, path):
|
||||
|
||||
if name in ("thumb", "fanart_image", "small_poster", "tiny_poster", "medium_landscape", "medium_poster", "small_fanartimage", "medium_fanartimage", "fanart_noindicators"):
|
||||
list.setProperty(name, path)
|
||||
else:
|
||||
list.setArt({name:path})
|
||||
|
||||
return list
|
||||
|
||||
def setListItemProps(self, server, id, listItem, result):
|
||||
def setArtwork(self, listItem):
|
||||
# Set up item and item info
|
||||
api = self.api
|
||||
item = self.item
|
||||
artwork = self.artwork
|
||||
|
||||
type = result.get('Type')
|
||||
people = api.getPeople(result)
|
||||
studios = api.getStudios(result)
|
||||
allartwork = artwork.getAllArtwork(item, parentInfo=True)
|
||||
# Set artwork for listitem
|
||||
arttypes = {
|
||||
|
||||
'poster': "Primary",
|
||||
'tvshow.poster': "Primary",
|
||||
'clearart': "Art",
|
||||
'tvshow.clearart': "Art",
|
||||
'clearlogo': "Logo",
|
||||
'tvshow.clearlogo': "Logo",
|
||||
'discart': "Disc",
|
||||
'fanart_image': "Backdrop",
|
||||
'landscape': "Thumb"
|
||||
}
|
||||
for arttype in arttypes:
|
||||
|
||||
art = arttypes[arttype]
|
||||
if art == "Backdrop":
|
||||
try: # Backdrop is a list, grab the first backdrop
|
||||
self.setArtProp(listItem, arttype, allartwork[art][0])
|
||||
except: pass
|
||||
else:
|
||||
self.setArtProp(listItem, arttype, allartwork[art])
|
||||
|
||||
def setArtProp(self, listItem, arttype, path):
|
||||
|
||||
if arttype in (
|
||||
'thumb', 'fanart_image', 'small_poster', 'tiny_poster',
|
||||
'medium_landscape', 'medium_poster', 'small_fanartimage',
|
||||
'medium_fanartimage', 'fanart_noindicators'):
|
||||
|
||||
listItem.setProperty(arttype, path)
|
||||
else:
|
||||
listItem.setArt({arttype: path})
|
||||
|
||||
def setListItem(self, listItem):
|
||||
|
||||
item = self.item
|
||||
type = item['Type']
|
||||
API = self.API
|
||||
people = API.getPeople()
|
||||
studios = API.getStudios()
|
||||
|
||||
metadata = {
|
||||
|
||||
'title': result.get('Name', "Missing name"),
|
||||
'year': result.get('ProductionYear'),
|
||||
'plot': api.getOverview(result),
|
||||
'title': item.get('Name', "Missing name"),
|
||||
'year': item.get('ProductionYear'),
|
||||
'plot': API.getOverview(),
|
||||
'director': people.get('Director'),
|
||||
'writer': people.get('Writer'),
|
||||
'mpaa': api.getMpaa(result),
|
||||
'genre': api.getGenre(result),
|
||||
'mpaa': API.getMpaa(),
|
||||
'genre': " / ".join(item['Genres']),
|
||||
'studio': " / ".join(studios),
|
||||
'aired': api.getPremiereDate(result),
|
||||
'rating': result.get('CommunityRating'),
|
||||
'votes': result.get('VoteCount')
|
||||
'aired': API.getPremiereDate(),
|
||||
'rating': item.get('CommunityRating'),
|
||||
'votes': item.get('VoteCount')
|
||||
}
|
||||
|
||||
if "Episode" in type:
|
||||
# Only for tv shows
|
||||
thumbId = result.get('SeriesId')
|
||||
season = result.get('ParentIndexNumber', -1)
|
||||
episode = result.get('IndexNumber', -1)
|
||||
show = result.get('SeriesName', "")
|
||||
thumbId = item.get('SeriesId')
|
||||
season = item.get('ParentIndexNumber', -1)
|
||||
episode = item.get('IndexNumber', -1)
|
||||
show = item.get('SeriesName', "")
|
||||
|
||||
metadata['TVShowTitle'] = show
|
||||
metadata['season'] = season
|
||||
|
@ -341,122 +351,3 @@ class PlaybackUtils():
|
|||
listItem.setProperty('IsFolder', 'false')
|
||||
listItem.setLabel(metadata['title'])
|
||||
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)'''
|
|
@ -2,45 +2,47 @@
|
|||
|
||||
#################################################################################################
|
||||
|
||||
import json as json
|
||||
import json
|
||||
|
||||
import xbmc
|
||||
import xbmcgui
|
||||
|
||||
from DownloadUtils import DownloadUtils
|
||||
from WebSocketClient import WebSocketThread
|
||||
from ClientInformation import ClientInformation
|
||||
from LibrarySync import LibrarySync
|
||||
import Utils as utils
|
||||
import utils
|
||||
import clientinfo
|
||||
import downloadutils
|
||||
import kodidb_functions as kodidb
|
||||
import websocket_client as wsc
|
||||
|
||||
#################################################################################################
|
||||
|
||||
class Player( xbmc.Player ):
|
||||
|
||||
class Player(xbmc.Player):
|
||||
|
||||
# Borg - multiple instances, shared state
|
||||
_shared_state = {}
|
||||
|
||||
xbmcplayer = xbmc.Player()
|
||||
doUtils = DownloadUtils()
|
||||
clientInfo = ClientInformation()
|
||||
ws = WebSocketThread()
|
||||
librarySync = LibrarySync()
|
||||
|
||||
addonName = clientInfo.getAddonName()
|
||||
|
||||
played_information = {}
|
||||
played_info = {}
|
||||
playStats = {}
|
||||
currentFile = None
|
||||
|
||||
def __init__(self, *args):
|
||||
|
||||
def __init__(self):
|
||||
|
||||
self.__dict__ = self._shared_state
|
||||
|
||||
self.clientInfo = clientinfo.ClientInfo()
|
||||
self.addonName = self.clientInfo.getAddonName()
|
||||
self.doUtils = downloadutils.DownloadUtils()
|
||||
self.ws = wsc.WebSocket_Client()
|
||||
self.xbmcplayer = xbmc.Player()
|
||||
|
||||
self.logMsg("Starting playback monitor.", 2)
|
||||
|
||||
def logMsg(self, msg, lvl=1):
|
||||
|
||||
self.className = self.__class__.__name__
|
||||
utils.logMsg("%s %s" % (self.addonName, self.className), msg, int(lvl))
|
||||
utils.logMsg("%s %s" % (self.addonName, self.className), msg, lvl)
|
||||
|
||||
|
||||
def GetPlayStats(self):
|
||||
return self.playStats
|
||||
|
@ -74,31 +76,42 @@ class Player( xbmc.Player ):
|
|||
self.currentFile = currentFile
|
||||
|
||||
# We may need to wait for info to be set in kodi monitor
|
||||
itemId = utils.window("%sitem_id" % currentFile)
|
||||
itemId = utils.window("emby_%s.itemid" % currentFile)
|
||||
tryCount = 0
|
||||
while not itemId:
|
||||
|
||||
xbmc.sleep(200)
|
||||
itemId = utils.window("%sitem_id" % currentFile)
|
||||
itemId = utils.window("emby_%s.itemid" % currentFile)
|
||||
if tryCount == 20: # try 20 times or about 10 seconds
|
||||
self.logMsg("Could not find itemId, cancelling playback report...", 1)
|
||||
break
|
||||
else: tryCount += 1
|
||||
|
||||
else:
|
||||
self.logMsg("ONPLAYBACK_STARTED: %s ITEMID: %s" % (currentFile, itemId), 0)
|
||||
self.logMsg("ONPLAYBACK_STARTED: %s itemid: %s" % (currentFile, itemId), 0)
|
||||
|
||||
# Only proceed if an itemId was found.
|
||||
runtime = utils.window("%sruntimeticks" % currentFile)
|
||||
refresh_id = utils.window("%srefresh_id" % currentFile)
|
||||
playMethod = utils.window("%splaymethod" % currentFile)
|
||||
itemType = utils.window("%stype" % currentFile)
|
||||
embyitem = "emby_%s" % currentFile
|
||||
runtime = utils.window("%s.runtime" % embyitem)
|
||||
refresh_id = utils.window("%s.refreshid" % embyitem)
|
||||
playMethod = utils.window("%s.playmethod" % embyitem)
|
||||
itemType = utils.window("%s.type" % embyitem)
|
||||
utils.window('emby_skipWatched%s' % itemId, value="true")
|
||||
|
||||
seekTime = xbmcplayer.getTime()
|
||||
|
||||
|
||||
# Get playback volume
|
||||
volume_query = '{"jsonrpc": "2.0", "method": "Application.GetProperties", "params": {"properties": ["volume","muted"]}, "id": 1}'
|
||||
result = xbmc.executeJSONRPC(volume_query)
|
||||
volume_query = {
|
||||
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "Application.GetProperties",
|
||||
"params": {
|
||||
|
||||
"properties": ["volume", "muted"]
|
||||
}
|
||||
}
|
||||
result = xbmc.executeJSONRPC(json.dumps(volume_query))
|
||||
result = json.loads(result)
|
||||
result = result.get('result')
|
||||
|
||||
|
@ -106,7 +119,7 @@ class Player( xbmc.Player ):
|
|||
muted = result.get('muted')
|
||||
|
||||
# Postdata structure to send to Emby server
|
||||
url = "{server}/mediabrowser/Sessions/Playing"
|
||||
url = "{server}/emby/Sessions/Playing"
|
||||
postdata = {
|
||||
|
||||
'QueueableMediaTypes': "Video",
|
||||
|
@ -123,12 +136,22 @@ class Player( xbmc.Player ):
|
|||
if playMethod == "Transcode":
|
||||
# property set in PlayUtils.py
|
||||
postdata['AudioStreamIndex'] = utils.window("%sAudioStreamIndex" % currentFile)
|
||||
postdata['SubtitleStreamIndex'] = utils.window("%sSubtitleStreamIndex" % currentFile)
|
||||
|
||||
postdata['SubtitleStreamIndex'] = utils.window("%sSubtitleStreamIndex"
|
||||
% currentFile)
|
||||
else:
|
||||
# Get the current kodi audio and subtitles and convert to Emby equivalent
|
||||
track_query = '{"jsonrpc": "2.0", "method": "Player.GetProperties", "params": {"playerid": 1,"properties": ["currentsubtitle","currentaudiostream","subtitleenabled"]} , "id": 1}'
|
||||
result = xbmc.executeJSONRPC(track_query)
|
||||
tracks_query = {
|
||||
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "Player.GetProperties",
|
||||
"params": {
|
||||
|
||||
"playerid": 1,
|
||||
"properties": ["currentsubtitle","currentaudiostream","subtitleenabled"]
|
||||
}
|
||||
}
|
||||
result = xbmc.executeJSONRPC(json.dumps(tracks_query))
|
||||
result = json.loads(result)
|
||||
result = result.get('result')
|
||||
|
||||
|
@ -155,9 +178,9 @@ class Player( xbmc.Player ):
|
|||
|
||||
# Number of audiotracks to help get Emby Index
|
||||
audioTracks = len(xbmc.Player().getAvailableAudioStreams())
|
||||
mapping = utils.window("%sIndexMapping" % currentFile)
|
||||
mapping = utils.window("%s.indexMapping" % embyitem)
|
||||
|
||||
if mapping: # Set in PlaybackUtils.py
|
||||
if mapping: # Set in playbackutils.py
|
||||
|
||||
self.logMsg("Mapping for external subtitles index: %s" % mapping, 2)
|
||||
externalIndex = json.loads(mapping)
|
||||
|
@ -167,7 +190,8 @@ class Player( xbmc.Player ):
|
|||
postdata['SubtitleStreamIndex'] = externalIndex[str(indexSubs)]
|
||||
else:
|
||||
# Internal subtitle currently selected
|
||||
postdata['SubtitleStreamIndex'] = indexSubs - len(externalIndex) + audioTracks + 1
|
||||
subindex = indexSubs - len(externalIndex) + audioTracks + 1
|
||||
postdata['SubtitleStreamIndex'] = subindex
|
||||
|
||||
else: # Direct paths enabled scenario or no external subtitles set
|
||||
postdata['SubtitleStreamIndex'] = indexSubs + audioTracks + 1
|
||||
|
@ -184,7 +208,7 @@ class Player( xbmc.Player ):
|
|||
runtime = int(runtime)
|
||||
except ValueError:
|
||||
runtime = xbmcplayer.getTotalTime()
|
||||
self.logMsg("Runtime is missing, grabbing runtime from Kodi player: %s" % runtime, 1)
|
||||
self.logMsg("Runtime is missing, Kodi runtime: %s" % runtime, 1)
|
||||
|
||||
# Save data map for updates and position calls
|
||||
data = {
|
||||
|
@ -200,8 +224,8 @@ class Player( xbmc.Player ):
|
|||
'currentPosition': int(seekTime)
|
||||
}
|
||||
|
||||
self.played_information[currentFile] = data
|
||||
self.logMsg("ADDING_FILE: %s" % self.played_information, 1)
|
||||
self.played_info[currentFile] = data
|
||||
self.logMsg("ADDING_FILE: %s" % self.played_info, 1)
|
||||
|
||||
# log some playback stats
|
||||
'''if(itemType != None):
|
||||
|
@ -225,7 +249,7 @@ class Player( xbmc.Player ):
|
|||
|
||||
# Get current file
|
||||
currentFile = self.currentFile
|
||||
data = self.played_information.get(currentFile)
|
||||
data = self.played_info.get(currentFile)
|
||||
|
||||
# only report playback if emby has initiated the playback (item_id has value)
|
||||
if data:
|
||||
|
@ -239,15 +263,23 @@ class Player( xbmc.Player ):
|
|||
|
||||
|
||||
# Get playback volume
|
||||
volume_query = '{"jsonrpc": "2.0", "method": "Application.GetProperties", "params": {"properties": ["volume","muted"]}, "id": 1}'
|
||||
result = xbmc.executeJSONRPC(volume_query)
|
||||
volume_query = {
|
||||
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "Application.GetProperties",
|
||||
"params": {
|
||||
|
||||
"properties": ["volume", "muted"]
|
||||
}
|
||||
}
|
||||
result = xbmc.executeJSONRPC(json.dumps(volume_query))
|
||||
result = json.loads(result)
|
||||
result = result.get('result')
|
||||
|
||||
volume = result.get('volume')
|
||||
muted = result.get('muted')
|
||||
|
||||
|
||||
# Postdata for the websocketclient report
|
||||
postdata = {
|
||||
|
||||
|
@ -269,8 +301,18 @@ class Player( xbmc.Player ):
|
|||
|
||||
else:
|
||||
# Get current audio and subtitles track
|
||||
track_query = '{"jsonrpc": "2.0", "method": "Player.GetProperties", "params": {"playerid":1,"properties": ["currentsubtitle","currentaudiostream","subtitleenabled"]} , "id": 1}'
|
||||
result = xbmc.executeJSONRPC(track_query)
|
||||
tracks_query = {
|
||||
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "Player.GetProperties",
|
||||
"params": {
|
||||
|
||||
"playerid": 1,
|
||||
"properties": ["currentsubtitle","currentaudiostream","subtitleenabled"]
|
||||
}
|
||||
}
|
||||
result = xbmc.executeJSONRPC(json.dumps(tracks_query))
|
||||
result = json.loads(result)
|
||||
result = result.get('result')
|
||||
|
||||
|
@ -297,7 +339,7 @@ class Player( xbmc.Player ):
|
|||
|
||||
# Number of audiotracks to help get Emby Index
|
||||
audioTracks = len(xbmc.Player().getAvailableAudioStreams())
|
||||
mapping = utils.window("%sIndexMapping" % currentFile)
|
||||
mapping = utils.window("emby_%s.indexMapping" % currentFile)
|
||||
|
||||
if mapping: # Set in PlaybackUtils.py
|
||||
|
||||
|
@ -306,13 +348,16 @@ class Player( xbmc.Player ):
|
|||
|
||||
if externalIndex.get(str(indexSubs)):
|
||||
# If the current subtitle is in the mapping
|
||||
data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = [externalIndex[str(indexSubs)]] * 2
|
||||
subindex = [externalIndex[str(indexSubs)]] * 2
|
||||
data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = subindex
|
||||
else:
|
||||
# Internal subtitle currently selected
|
||||
data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = [indexSubs - len(externalIndex) + audioTracks + 1] * 2
|
||||
subindex = [indexSubs - len(externalIndex) + audioTracks + 1] * 2
|
||||
data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = subindex
|
||||
|
||||
else: # Direct paths enabled scenario or no external subtitles set
|
||||
data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = [indexSubs + audioTracks + 1] * 2
|
||||
subindex = [indexSubs + audioTracks + 1] * 2
|
||||
data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = subindex
|
||||
else:
|
||||
data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = [""] * 2
|
||||
|
||||
|
@ -326,8 +371,8 @@ class Player( xbmc.Player ):
|
|||
currentFile = self.currentFile
|
||||
self.logMsg("PLAYBACK_PAUSED: %s" % currentFile, 2)
|
||||
|
||||
if self.played_information.get(currentFile):
|
||||
self.played_information[currentFile]['paused'] = True
|
||||
if self.played_info.get(currentFile):
|
||||
self.played_info[currentFile]['paused'] = True
|
||||
|
||||
self.reportPlayback()
|
||||
|
||||
|
@ -336,8 +381,8 @@ class Player( xbmc.Player ):
|
|||
currentFile = self.currentFile
|
||||
self.logMsg("PLAYBACK_RESUMED: %s" % currentFile, 2)
|
||||
|
||||
if self.played_information.get(currentFile):
|
||||
self.played_information[currentFile]['paused'] = False
|
||||
if self.played_info.get(currentFile):
|
||||
self.played_info[currentFile]['paused'] = False
|
||||
|
||||
self.reportPlayback()
|
||||
|
||||
|
@ -346,15 +391,17 @@ class Player( xbmc.Player ):
|
|||
currentFile = self.currentFile
|
||||
self.logMsg("PLAYBACK_SEEK: %s" % currentFile, 2)
|
||||
|
||||
if self.played_information.get(currentFile):
|
||||
if self.played_info.get(currentFile):
|
||||
position = self.xbmcplayer.getTime()
|
||||
self.played_information[currentFile]['currentPosition'] = position
|
||||
self.played_info[currentFile]['currentPosition'] = position
|
||||
|
||||
self.reportPlayback()
|
||||
|
||||
def onPlayBackStopped( self ):
|
||||
# Will be called when user stops xbmc playing a file
|
||||
self.logMsg("ONPLAYBACK_STOPPED", 2)
|
||||
xbmcgui.Window(10101).clearProperties()
|
||||
self.logMsg("Clear playlist properties.")
|
||||
self.stopAll()
|
||||
|
||||
def onPlayBackEnded( self ):
|
||||
|
@ -364,14 +411,16 @@ class Player( xbmc.Player ):
|
|||
|
||||
def stopAll(self):
|
||||
|
||||
if not self.played_information:
|
||||
doUtils = self.doUtils
|
||||
|
||||
if not self.played_info:
|
||||
return
|
||||
|
||||
self.logMsg("Played_information: %s" % self.played_information, 1)
|
||||
self.logMsg("Played_information: %s" % self.played_info, 1)
|
||||
# Process each items
|
||||
for item in self.played_information:
|
||||
for item in self.played_info:
|
||||
|
||||
data = self.played_information.get(item)
|
||||
data = self.played_info.get(item)
|
||||
if data:
|
||||
|
||||
self.logMsg("Item path: %s" % item, 2)
|
||||
|
@ -379,47 +428,61 @@ class Player( xbmc.Player ):
|
|||
|
||||
runtime = data['runtime']
|
||||
currentPosition = data['currentPosition']
|
||||
itemId = data['item_id']
|
||||
itemid = data['item_id']
|
||||
refresh_id = data['refresh_id']
|
||||
currentFile = data['currentfile']
|
||||
type = data['Type']
|
||||
playMethod = data['playmethod']
|
||||
|
||||
if currentPosition and runtime:
|
||||
percentComplete = (currentPosition * 10000000) / int(runtime)
|
||||
markPlayedAt = float(utils.settings('markPlayed')) / 100
|
||||
try:
|
||||
percentComplete = (currentPosition * 10000000) / int(runtime)
|
||||
except ZeroDivisionError:
|
||||
# Runtime is 0.
|
||||
percentComplete = 0
|
||||
|
||||
self.logMsg("Percent complete: %s Mark played at: %s" % (percentComplete, markPlayedAt), 1)
|
||||
# Prevent manually mark as watched in Kodi monitor > WriteKodiVideoDB().UpdatePlaycountFromKodi()
|
||||
utils.window('SkipWatched%s' % itemId, "true")
|
||||
markPlayedAt = float(utils.settings('markPlayed')) / 100
|
||||
self.logMsg(
|
||||
"Percent complete: %s Mark played at: %s"
|
||||
% (percentComplete, markPlayedAt), 1)
|
||||
|
||||
# Prevent manually mark as watched in Kodi monitor
|
||||
utils.window('emby_skipWatched%s' % itemid, value="true")
|
||||
|
||||
self.stopPlayback(data)
|
||||
offerDelete = utils.settings('offerDelete') == "true"
|
||||
offerTypeDelete = False
|
||||
# Stop transcoding
|
||||
if playMethod == "Transcode":
|
||||
self.logMsg("Transcoding for %s terminated." % itemid, 1)
|
||||
deviceId = self.clientInfo.getDeviceId()
|
||||
url = "{server}/emby/Videos/ActiveEncodings?DeviceId=%s" % deviceId
|
||||
doUtils.downloadUrl(url, type="DELETE")
|
||||
|
||||
if type == "Episode" and utils.settings('offerDeleteTV') == "true":
|
||||
offerTypeDelete = True
|
||||
# Send the delete action to the server.
|
||||
offerDelete = False
|
||||
|
||||
elif type == "Movie" and utils.settings('offerDeleteMovies') == "true":
|
||||
offerTypeDelete = True
|
||||
if type == "Episode" and utils.settings('deleteTV') == "true":
|
||||
offerDelete = True
|
||||
elif type == "Movie" and utils.settings('deleteMovies') == "true":
|
||||
offerDelete = True
|
||||
|
||||
if percentComplete >= markPlayedAt and offerDelete and offerTypeDelete:
|
||||
# Make the bigger setting be able to disable option easily.
|
||||
self.logMsg("Offering deletion for: %s." % itemId, 1)
|
||||
return_value = xbmcgui.Dialog().yesno("Offer Delete", "Delete %s" % currentFile.split("/")[-1], "on Emby Server?")
|
||||
if return_value:
|
||||
# Delete Kodi entry before Emby
|
||||
listItem = [itemId]
|
||||
LibrarySync().removefromDB(listItem, True)
|
||||
if utils.settings('offerDelete') != "true":
|
||||
# Delete could be disabled, even if the subsetting is enabled.
|
||||
offerDelete = False
|
||||
|
||||
# Stop transcoding
|
||||
if playMethod == "Transcode":
|
||||
self.logMsg("Transcoding for %s terminated." % itemId, 1)
|
||||
deviceId = self.clientInfo.getMachineId()
|
||||
url = "{server}/mediabrowser/Videos/ActiveEncodings?DeviceId=%s" % deviceId
|
||||
self.doUtils.downloadUrl(url, type="DELETE")
|
||||
if percentComplete >= markPlayedAt and offerDelete:
|
||||
if utils.settings('skipConfirmDelete') != "true":
|
||||
resp = xbmcgui.Dialog().yesno(
|
||||
heading="Confirm delete",
|
||||
line1="Delete file on Emby Server?")
|
||||
if not resp:
|
||||
self.logMsg("User skipped deletion.", 1)
|
||||
continue
|
||||
|
||||
self.played_information.clear()
|
||||
url = "{server}/emby/Items/%s?format=json" % itemid
|
||||
self.logMsg("Deleting request: %s" % itemid)
|
||||
doUtils.downloadUrl(url, type="DELETE")
|
||||
|
||||
self.played_info.clear()
|
||||
|
||||
def stopPlayback(self, data):
|
||||
|
||||
|
@ -429,12 +492,11 @@ class Player( xbmc.Player ):
|
|||
currentPosition = data['currentPosition']
|
||||
positionTicks = int(currentPosition * 10000000)
|
||||
|
||||
url = "{server}/mediabrowser/Sessions/Playing/Stopped"
|
||||
url = "{server}/emby/Sessions/Playing/Stopped"
|
||||
postdata = {
|
||||
|
||||
'ItemId': itemId,
|
||||
'MediaSourceId': itemId,
|
||||
'PositionTicks': positionTicks
|
||||
}
|
||||
|
||||
self.doUtils.downloadUrl(url, postBody=postdata, type="POST")
|
|
@ -1,22 +1,21 @@
|
|||
#################################################################################################
|
||||
# UserClient thread
|
||||
#################################################################################################
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
##################################################################################################
|
||||
|
||||
import hashlib
|
||||
import threading
|
||||
|
||||
import xbmc
|
||||
import xbmcgui
|
||||
import xbmcaddon
|
||||
import xbmcvfs
|
||||
|
||||
import threading
|
||||
import hashlib
|
||||
import json as json
|
||||
import artwork
|
||||
import utils
|
||||
import clientinfo
|
||||
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):
|
||||
|
@ -24,16 +23,7 @@ class UserClient(threading.Thread):
|
|||
# Borg - multiple instances, shared state
|
||||
_shared_state = {}
|
||||
|
||||
clientInfo = ClientInformation()
|
||||
doUtils = DownloadUtils()
|
||||
KodiMonitor = KodiMonitor.Kodi_Monitor()
|
||||
|
||||
addonName = clientInfo.getAddonName()
|
||||
addon = xbmcaddon.Addon()
|
||||
WINDOW = xbmcgui.Window(10000)
|
||||
|
||||
stopClient = False
|
||||
logLevel = int(addon.getSetting('logLevel'))
|
||||
auth = True
|
||||
retry = 0
|
||||
|
||||
|
@ -44,25 +34,25 @@ class UserClient(threading.Thread):
|
|||
HasAccess = True
|
||||
AdditionalUser = []
|
||||
|
||||
def __init__(self, *args):
|
||||
userSettings = None
|
||||
|
||||
|
||||
def __init__(self):
|
||||
|
||||
self.__dict__ = self._shared_state
|
||||
threading.Thread.__init__(self, *args)
|
||||
self.addon = xbmcaddon.Addon()
|
||||
|
||||
self.addonName = clientinfo.ClientInfo().getAddonName()
|
||||
self.doUtils = downloadutils.DownloadUtils()
|
||||
self.logLevel = int(utils.settings('logLevel'))
|
||||
|
||||
threading.Thread.__init__(self)
|
||||
|
||||
def logMsg(self, msg, lvl=1):
|
||||
|
||||
className = self.__class__.__name__
|
||||
utils.logMsg("%s %s" % (self.addonName, className), str(msg), int(lvl))
|
||||
utils.logMsg("%s %s" % (self.addonName, className), msg, lvl)
|
||||
|
||||
def getUsername(self):
|
||||
|
||||
username = utils.settings('username')
|
||||
|
||||
if (username == ""):
|
||||
self.logMsg("No username saved.", 2)
|
||||
return ""
|
||||
|
||||
return username
|
||||
|
||||
def getAdditionalUsers(self):
|
||||
|
||||
|
@ -71,11 +61,21 @@ class UserClient(threading.Thread):
|
|||
if additionalUsers:
|
||||
self.AdditionalUser = additionalUsers.split(',')
|
||||
|
||||
def getUsername(self):
|
||||
|
||||
username = utils.settings('username')
|
||||
|
||||
if not username:
|
||||
self.logMsg("No username saved.", 2)
|
||||
return ""
|
||||
|
||||
return username
|
||||
|
||||
def getLogLevel(self):
|
||||
|
||||
try:
|
||||
logLevel = int(utils.settings('logLevel'))
|
||||
except:
|
||||
except ValueError:
|
||||
logLevel = 0
|
||||
|
||||
return logLevel
|
||||
|
@ -83,71 +83,84 @@ class UserClient(threading.Thread):
|
|||
def getUserId(self):
|
||||
|
||||
username = self.getUsername()
|
||||
w_userId = self.WINDOW.getProperty('userId%s' % username)
|
||||
w_userId = utils.window('emby_userId%s' % username)
|
||||
s_userId = utils.settings('userId%s' % username)
|
||||
|
||||
# Verify the window property
|
||||
if (w_userId != ""):
|
||||
self.logMsg("Returning userId from WINDOW for username: %s UserId: %s" % (username, w_userId), 2)
|
||||
if w_userId:
|
||||
if not s_userId:
|
||||
# Save access token if it's missing from settings
|
||||
utils.settings('userId%s' % username, value=w_userId)
|
||||
self.logMsg(
|
||||
"Returning userId from WINDOW for username: %s UserId: %s"
|
||||
% (username, w_userId), 2)
|
||||
return w_userId
|
||||
# Verify the settings
|
||||
elif (s_userId != ""):
|
||||
self.logMsg("Returning userId from SETTINGS for username: %s userId: %s" % (username, s_userId), 2)
|
||||
elif s_userId:
|
||||
self.logMsg(
|
||||
"Returning userId from SETTINGS for username: %s userId: %s"
|
||||
% (username, s_userId), 2)
|
||||
return s_userId
|
||||
# No userId found
|
||||
else:
|
||||
self.logMsg("No userId saved for username: %s." % username)
|
||||
return
|
||||
self.logMsg("No userId saved for username: %s." % username, 1)
|
||||
|
||||
def getServer(self, prefix=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:
|
||||
HTTPS = utils.settings('secondhttps')
|
||||
# Alternate host
|
||||
HTTPS = utils.settings('secondhttps') == "true"
|
||||
host = utils.settings('secondipaddress')
|
||||
port = utils.settings('secondport')
|
||||
else:
|
||||
# Original host
|
||||
HTTPS = utils.settings('https') == "true"
|
||||
host = utils.settings('ipaddress')
|
||||
port = utils.settings('port')
|
||||
|
||||
server = host + ":" + port
|
||||
|
||||
if host == "":
|
||||
if not host:
|
||||
self.logMsg("No server information saved.", 2)
|
||||
return ""
|
||||
return False
|
||||
|
||||
# If https is true
|
||||
if prefix and (HTTPS == "true"):
|
||||
if prefix and HTTPS:
|
||||
server = "https://%s" % server
|
||||
return server
|
||||
# If https is false
|
||||
elif prefix and (HTTPS == "false"):
|
||||
elif prefix and not HTTPS:
|
||||
server = "http://%s" % server
|
||||
return server
|
||||
# If only the host:port is required
|
||||
elif (prefix == False):
|
||||
elif not prefix:
|
||||
return server
|
||||
|
||||
def getToken(self):
|
||||
|
||||
username = self.getUsername()
|
||||
w_token = self.WINDOW.getProperty('accessToken%s' % username)
|
||||
w_token = utils.window('emby_accessToken%s' % username)
|
||||
s_token = utils.settings('accessToken')
|
||||
|
||||
# Verify the window property
|
||||
if (w_token != ""):
|
||||
self.logMsg("Returning accessToken from WINDOW for username: %s accessToken: %s" % (username, w_token), 2)
|
||||
if w_token:
|
||||
if not s_token:
|
||||
# Save access token if it's missing from settings
|
||||
utils.settings('accessToken', value=w_token)
|
||||
self.logMsg(
|
||||
"Returning accessToken from WINDOW for username: %s accessToken: %s"
|
||||
% (username, w_token), 2)
|
||||
return w_token
|
||||
# Verify the settings
|
||||
elif (s_token != ""):
|
||||
self.logMsg("Returning accessToken from SETTINGS for username: %s accessToken: %s" % (username, s_token), 2)
|
||||
self.WINDOW.setProperty('accessToken%s' % username, s_token)
|
||||
elif s_token:
|
||||
self.logMsg(
|
||||
"Returning accessToken from SETTINGS for username: %s accessToken: %s"
|
||||
% (username, s_token), 2)
|
||||
utils.window('emby_accessToken%s' % username, value=s_token)
|
||||
return s_token
|
||||
else:
|
||||
self.logMsg("No token found.")
|
||||
self.logMsg("No token found.", 1)
|
||||
return ""
|
||||
|
||||
def getSSLverify(self):
|
||||
|
@ -174,71 +187,63 @@ class UserClient(threading.Thread):
|
|||
|
||||
def setUserPref(self):
|
||||
|
||||
player = Player()
|
||||
server = self.getServer()
|
||||
userId = self.getUserId()
|
||||
|
||||
url = "{server}/mediabrowser/Users/{UserId}?format=json"
|
||||
result = self.doUtils.downloadUrl(url)
|
||||
doUtils = self.doUtils
|
||||
|
||||
url = "{server}/emby/Users/{UserId}?format=json"
|
||||
result = doUtils.downloadUrl(url)
|
||||
self.userSettings = result
|
||||
# Set user image for skin display
|
||||
self.WINDOW.setProperty("EmbyUserImage",API().getUserArtwork(result,"Primary"))
|
||||
if result.get('PrimaryImageTag'):
|
||||
utils.window('EmbyUserImage', value=artwork.Artwork().getUserArtwork(result, 'Primary'))
|
||||
|
||||
# Load the resume point from Emby and set as setting
|
||||
url = "{server}/mediabrowser/System/Configuration?format=json"
|
||||
result = self.doUtils.downloadUrl(url)
|
||||
# Set resume point max
|
||||
url = "{server}/emby/System/Configuration?format=json"
|
||||
result = doUtils.downloadUrl(url)
|
||||
|
||||
utils.settings('markPlayed', value=str(result['MaxResumePct']))
|
||||
|
||||
return True
|
||||
|
||||
def getPublicUsers(self):
|
||||
|
||||
server = self.getServer()
|
||||
|
||||
# Get public Users
|
||||
url = "%s/mediabrowser/Users/Public?format=json" % server
|
||||
url = "%s/emby/Users/Public?format=json" % server
|
||||
result = self.doUtils.downloadUrl(url, authenticate=False)
|
||||
|
||||
users = []
|
||||
|
||||
if (result != ""):
|
||||
users = result
|
||||
if result != "":
|
||||
return result
|
||||
else:
|
||||
# Server connection failed
|
||||
return False
|
||||
|
||||
return users
|
||||
|
||||
def hasAccess(self):
|
||||
|
||||
url = "{server}/mediabrowser/Users"
|
||||
# hasAccess is verified in service.py
|
||||
url = "{server}/emby/Users?format=json"
|
||||
result = self.doUtils.downloadUrl(url)
|
||||
|
||||
if result is False:
|
||||
# Access is restricted
|
||||
self.logMsg("Access is restricted.")
|
||||
if result == False:
|
||||
# Access is restricted, set in downloadutils.py via exception
|
||||
self.logMsg("Access is restricted.", 1)
|
||||
self.HasAccess = False
|
||||
return
|
||||
elif self.WINDOW.getProperty('Server_online') != "true":
|
||||
# Server connection failed
|
||||
return
|
||||
|
||||
if self.WINDOW.getProperty("Server_status") == "restricted":
|
||||
self.logMsg("Access is granted.")
|
||||
elif utils.window('emby_online') != "true":
|
||||
# Server connection failed
|
||||
pass
|
||||
|
||||
elif utils.window('emby_serverStatus') == "restricted":
|
||||
self.logMsg("Access is granted.", 1)
|
||||
self.HasAccess = True
|
||||
self.WINDOW.setProperty("Server_status", "")
|
||||
utils.window('emby_serverStatus', clear=True)
|
||||
xbmcgui.Dialog().notification("Emby server", "Access is enabled.")
|
||||
return
|
||||
|
||||
def loadCurrUser(self, authenticated=False):
|
||||
|
||||
WINDOW = self.WINDOW
|
||||
doUtils = self.doUtils
|
||||
username = self.getUsername()
|
||||
userId = self.getUserId()
|
||||
|
||||
# Only to be used if token exists
|
||||
self.currUserId = self.getUserId()
|
||||
self.currUserId = userId
|
||||
self.currServer = self.getServer()
|
||||
self.currToken = self.getToken()
|
||||
self.ssl = self.getSSLverify()
|
||||
|
@ -246,21 +251,21 @@ class UserClient(threading.Thread):
|
|||
|
||||
# Test the validity of current token
|
||||
if authenticated == False:
|
||||
url = "%s/mediabrowser/Users/%s" % (self.currServer, self.currUserId)
|
||||
WINDOW.setProperty("currUser", username)
|
||||
WINDOW.setProperty("accessToken%s" % username, self.currToken)
|
||||
url = "%s/emby/Users/%s?format=json" % (self.currServer, userId)
|
||||
utils.window('emby_currUser', value=userId)
|
||||
utils.window('emby_accessToken%s' % userId, value=self.currToken)
|
||||
result = doUtils.downloadUrl(url)
|
||||
|
||||
if result == 401:
|
||||
# Token is no longer valid
|
||||
self.resetClient()
|
||||
return False
|
||||
|
||||
# Set to windows property
|
||||
WINDOW.setProperty("currUser", username)
|
||||
WINDOW.setProperty("accessToken%s" % username, self.currToken)
|
||||
WINDOW.setProperty("server%s" % username, self.currServer)
|
||||
WINDOW.setProperty("server_%s" % username, self.getServer(prefix=False))
|
||||
WINDOW.setProperty("userId%s" % username, self.currUserId)
|
||||
utils.window('emby_currUser', value=userId)
|
||||
utils.window('emby_accessToken%s' % userId, value=self.currToken)
|
||||
utils.window('emby_server%s' % userId, value=self.currServer)
|
||||
utils.window('emby_server_%s' % userId, value=self.getServer(prefix=False))
|
||||
|
||||
# Set DownloadUtils values
|
||||
doUtils.setUsername(username)
|
||||
|
@ -273,188 +278,194 @@ class UserClient(threading.Thread):
|
|||
# Start DownloadUtils session
|
||||
doUtils.startSession()
|
||||
self.getAdditionalUsers()
|
||||
|
||||
self.currUser = username
|
||||
# Set user preferences in settings
|
||||
self.currUser = username
|
||||
self.setUserPref()
|
||||
|
||||
def authenticate(self):
|
||||
|
||||
WINDOW = self.WINDOW
|
||||
addon = self.addon
|
||||
def authenticate(self):
|
||||
# Get /profile/addon_data
|
||||
addondir = xbmc.translatePath(self.addon.getAddonInfo('profile')).decode('utf-8')
|
||||
hasSettings = xbmcvfs.exists("%ssettings.xml" % addondir)
|
||||
|
||||
username = self.getUsername()
|
||||
server = self.getServer()
|
||||
addondir = xbmc.translatePath(self.addon.getAddonInfo('profile')).decode('utf-8')
|
||||
hasSettings = xbmcvfs.exists("%ssettings.xml" % addondir)
|
||||
|
||||
# If there's no settings.xml
|
||||
if (hasSettings == 0):
|
||||
self.logMsg("No settings.xml found.")
|
||||
if not hasSettings:
|
||||
self.logMsg("No settings.xml found.", 1)
|
||||
self.auth = False
|
||||
return
|
||||
# If no user information
|
||||
if (server == "") or (username == ""):
|
||||
self.logMsg("Missing server information.")
|
||||
elif not server or not username:
|
||||
self.logMsg("Missing server information.", 1)
|
||||
self.auth = False
|
||||
return
|
||||
# If there's a token
|
||||
if (self.getToken() != ""):
|
||||
# If there's a token, load the user
|
||||
elif self.getToken():
|
||||
result = self.loadCurrUser()
|
||||
|
||||
if result == False:
|
||||
pass
|
||||
else:
|
||||
self.logMsg("Current user: %s" % self.currUser, 0)
|
||||
self.logMsg("Current userId: %s" % self.currUserId, 0)
|
||||
self.logMsg("Current accessToken: %s" % self.currToken, 0)
|
||||
self.logMsg("Current user: %s" % self.currUser, 1)
|
||||
self.logMsg("Current userId: %s" % self.currUserId, 1)
|
||||
self.logMsg("Current accessToken: %s" % self.currToken, 2)
|
||||
return
|
||||
|
||||
##### AUTHENTICATE USER #####
|
||||
|
||||
users = self.getPublicUsers()
|
||||
password = ""
|
||||
|
||||
# Find user in list
|
||||
for user in users:
|
||||
name = user[u'Name']
|
||||
userHasPassword = False
|
||||
name = user['Name']
|
||||
|
||||
if (unicode(username, 'utf-8') in name):
|
||||
# Verify if user has a password
|
||||
if (user.get("HasPassword") == True):
|
||||
userHasPassword = True
|
||||
if username.decode('utf-8') in name:
|
||||
# If user has password
|
||||
if (userHasPassword):
|
||||
password = xbmcgui.Dialog().input("Enter password for user: %s" % username, option=xbmcgui.ALPHANUM_HIDE_INPUT)
|
||||
if user['HasPassword'] == True:
|
||||
password = xbmcgui.Dialog().input(
|
||||
heading="Enter password for user: %s" % username,
|
||||
option=xbmcgui.ALPHANUM_HIDE_INPUT)
|
||||
# If password dialog is cancelled
|
||||
if (password == ""):
|
||||
if not password:
|
||||
self.logMsg("No password entered.", 0)
|
||||
self.WINDOW.setProperty("Server_status", "Stop")
|
||||
utils.window('emby_serverStatus', value="Stop")
|
||||
self.auth = False
|
||||
return
|
||||
break
|
||||
else:
|
||||
# Manual login, user is hidden
|
||||
password = xbmcgui.Dialog().input("Enter password for user: %s" % username, option=xbmcgui.ALPHANUM_HIDE_INPUT)
|
||||
|
||||
password = xbmcgui.Dialog().input(
|
||||
heading="Enter password for user: %s" % username,
|
||||
option=xbmcgui.ALPHANUM_HIDE_INPUT)
|
||||
sha1 = hashlib.sha1(password)
|
||||
sha1 = sha1.hexdigest()
|
||||
|
||||
# Authenticate username and password
|
||||
url = "%s/mediabrowser/Users/AuthenticateByName?format=json" % server
|
||||
url = "%s/emby/Users/AuthenticateByName?format=json" % server
|
||||
data = {'username': username, 'password': sha1}
|
||||
self.logMsg(data, 2)
|
||||
|
||||
result = self.doUtils.downloadUrl(url, postBody=data, type="POST", authenticate=False)
|
||||
|
||||
accessToken = None
|
||||
try:
|
||||
self.logMsg("Auth_Reponse: %s" % result, 1)
|
||||
accessToken = result[u'AccessToken']
|
||||
except:
|
||||
pass
|
||||
self.logMsg("Auth response: %s" % result, 1)
|
||||
accessToken = result['AccessToken']
|
||||
|
||||
if (result != None and accessToken != None):
|
||||
except (KeyError, TypeError):
|
||||
self.logMsg("Failed to retrieve the api key.", 1)
|
||||
accessToken = None
|
||||
|
||||
if accessToken is not None:
|
||||
self.currUser = username
|
||||
xbmcgui.Dialog().notification("Emby server", "Welcome %s!" % self.currUser)
|
||||
userId = result[u'User'][u'Id']
|
||||
utils.settings("accessToken", accessToken)
|
||||
utils.settings("userId%s" % username, userId)
|
||||
self.logMsg("User Authenticated: %s" % accessToken)
|
||||
userId = result['User']['Id']
|
||||
utils.settings('accessToken', value=accessToken)
|
||||
utils.settings('userId%s' % username, value=userId)
|
||||
self.logMsg("User Authenticated: %s" % accessToken, 1)
|
||||
self.loadCurrUser(authenticated=True)
|
||||
self.WINDOW.setProperty("Server_status", "")
|
||||
utils.window('emby_serverStatus', clear=True)
|
||||
self.retry = 0
|
||||
return
|
||||
else:
|
||||
self.logMsg("User authentication failed.")
|
||||
utils.settings("accessToken", "")
|
||||
utils.settings("userId%s" % username, "")
|
||||
self.logMsg("User authentication failed.", 1)
|
||||
utils.settings('accessToken', value="")
|
||||
utils.settings('userId%s' % username, value="")
|
||||
xbmcgui.Dialog().ok("Error connecting", "Invalid username or password.")
|
||||
|
||||
# Give two attempts at entering password
|
||||
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.logMsg(
|
||||
"""Too many retries. You can retry by resetting
|
||||
attempts in the addon settings.""", 1)
|
||||
utils.window('emby_serverStatus', value="Stop")
|
||||
xbmcgui.Dialog().ok(
|
||||
heading="Error connecting",
|
||||
line1="Failed to authenticate too many times.",
|
||||
line2="You can retry by resetting attempts in the addon settings.")
|
||||
|
||||
self.retry += 1
|
||||
self.auth = False
|
||||
return
|
||||
|
||||
def resetClient(self):
|
||||
|
||||
username = self.getUsername()
|
||||
self.logMsg("Reset UserClient authentication.", 1)
|
||||
if (self.currToken != None):
|
||||
username = self.getUsername()
|
||||
|
||||
if self.currToken is not None:
|
||||
# In case of 401, removed saved token
|
||||
utils.settings("accessToken", "")
|
||||
self.WINDOW.setProperty("accessToken%s" % username, "")
|
||||
utils.settings('accessToken', value="")
|
||||
utils.window('emby_accessToken%s' % username, clear=True)
|
||||
self.currToken = None
|
||||
self.logMsg("User token has been removed.", 1)
|
||||
|
||||
self.auth = True
|
||||
self.currUser = None
|
||||
return
|
||||
|
||||
|
||||
def run(self):
|
||||
|
||||
self.logMsg("|---- Starting UserClient ----|", 0)
|
||||
monitor = xbmc.Monitor()
|
||||
self.logMsg("----===## Starting UserClient ##===----", 0)
|
||||
|
||||
while not self.KodiMonitor.abortRequested():
|
||||
while not monitor.abortRequested():
|
||||
|
||||
# Verify the log level
|
||||
currLogLevel = self.getLogLevel()
|
||||
if self.logLevel != currLogLevel:
|
||||
# Set new log level
|
||||
self.logLevel = currLogLevel
|
||||
utils.window('emby_logLevel', value=str(currLogLevel))
|
||||
self.logMsg("New Log Level: %s" % currLogLevel, 0)
|
||||
self.WINDOW.setProperty('getLogLevel', str(currLogLevel))
|
||||
|
||||
if (self.WINDOW.getProperty("Server_status") != ""):
|
||||
status = self.WINDOW.getProperty("Server_status")
|
||||
|
||||
status = utils.window('emby_serverStatus')
|
||||
if status:
|
||||
# Verify the connection status to server
|
||||
if status == "restricted":
|
||||
# Parental control is restricting access
|
||||
self.HasAccess = False
|
||||
|
||||
elif status == "401":
|
||||
self.WINDOW.setProperty("Server_status", "Auth")
|
||||
# Revoked token
|
||||
# Unauthorized access, revoke token
|
||||
utils.window('emby_serverStatus', value="Auth")
|
||||
self.resetClient()
|
||||
|
||||
if self.auth and (self.currUser == None):
|
||||
status = self.WINDOW.getProperty("Server_status")
|
||||
|
||||
if (status == "") or (status == "Auth"):
|
||||
if self.auth and (self.currUser is None):
|
||||
# Try to authenticate user
|
||||
status = utils.window('emby_serverStatus')
|
||||
if not status or status == "Auth":
|
||||
# Set auth flag because we no longer need
|
||||
# to authenticate the user
|
||||
self.auth = False
|
||||
self.authenticate()
|
||||
|
||||
if (self.auth == False) and (self.currUser == None):
|
||||
# Only if there's information found to login
|
||||
|
||||
if not self.auth and (self.currUser is None):
|
||||
# If authenticate failed.
|
||||
server = self.getServer()
|
||||
username = self.getUsername()
|
||||
status = self.WINDOW.getProperty("Server_status")
|
||||
status = utils.window('emby_serverStatus')
|
||||
|
||||
# If user didn't enter a password when prompted
|
||||
if status == "Stop":
|
||||
pass
|
||||
|
||||
elif (server != "") and (username != ""):
|
||||
self.logMsg("Server found: %s" % server)
|
||||
self.logMsg("Username found: %s" % username)
|
||||
# The status Stop is for when user cancelled password dialog.
|
||||
if server and username and status != "Stop":
|
||||
# Only if there's information found to login
|
||||
self.logMsg("Server found: %s" % server, 2)
|
||||
self.logMsg("Username found: %s" % username, 2)
|
||||
self.auth = True
|
||||
|
||||
# If stopping the client didn't work
|
||||
|
||||
if self.stopClient == True:
|
||||
# If stopping the client didn't work
|
||||
break
|
||||
|
||||
if self.KodiMonitor.waitForAbort(1):
|
||||
if monitor.waitForAbort(1):
|
||||
# Abort was requested while waiting. We should exit
|
||||
break
|
||||
|
||||
self.doUtils.stopSession()
|
||||
self.logMsg("|---- UserClient Stopped ----|", 0)
|
||||
self.logMsg("##===---- UserClient Stopped ----===##", 0)
|
||||
|
||||
def stopClient(self):
|
||||
# As last resort
|
||||
# When emby for kodi terminates
|
||||
self.stopClient = True
|
|
@ -1,59 +1,76 @@
|
|||
#################################################################################################
|
||||
# utils
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
#################################################################################################
|
||||
|
||||
import xbmc
|
||||
import xbmcgui
|
||||
import xbmcaddon
|
||||
import xbmcvfs
|
||||
import json
|
||||
import os
|
||||
import cProfile
|
||||
import pstats
|
||||
import time
|
||||
import inspect
|
||||
import pstats
|
||||
import sqlite3
|
||||
import string
|
||||
import time
|
||||
import unicodedata
|
||||
import xml.etree.ElementTree as etree
|
||||
|
||||
from API import API
|
||||
from PlayUtils import PlayUtils
|
||||
from DownloadUtils import DownloadUtils
|
||||
import xbmc
|
||||
import xbmcaddon
|
||||
import xbmcgui
|
||||
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
|
||||
logLevel = int(WINDOW.getProperty('getLogLevel'))
|
||||
try:
|
||||
logLevel = int(window('emby_logLevel'))
|
||||
except ValueError:
|
||||
logLevel = 0
|
||||
|
||||
if(logLevel >= level):
|
||||
if(logLevel == 2): # inspect.stack() is expensive
|
||||
if logLevel >= level:
|
||||
|
||||
if logLevel == 2: # inspect.stack() is expensive
|
||||
try:
|
||||
xbmc.log(title + " -> " + inspect.stack()[1][3] + " : " + str(msg))
|
||||
xbmc.log("%s -> %s : %s" % (title, inspect.stack()[1][3], msg))
|
||||
except UnicodeEncodeError:
|
||||
xbmc.log(title + " -> " + inspect.stack()[1][3] + " : " + str(msg.encode('utf-8')))
|
||||
xbmc.log("%s -> %s : %s" % (title, inspect.stack()[1][3], msg.encode('utf-8')))
|
||||
else:
|
||||
try:
|
||||
xbmc.log(title + " -> " + str(msg))
|
||||
xbmc.log("%s -> %s" % (title, msg))
|
||||
except UnicodeEncodeError:
|
||||
xbmc.log(title + " -> " + str(msg.encode('utf-8')))
|
||||
xbmc.log("%s -> %s" % (title, msg.encode('utf-8')))
|
||||
|
||||
def convertEncoding(data):
|
||||
#nasty hack to make sure we have a unicode string
|
||||
try:
|
||||
return data.decode('utf-8')
|
||||
except:
|
||||
return data
|
||||
def window(property, value=None, clear=False, windowid=10000):
|
||||
# Get or set window property
|
||||
WINDOW = xbmcgui.Window(windowid)
|
||||
|
||||
def KodiSQL(type="video"):
|
||||
if clear:
|
||||
WINDOW.clearProperty(property)
|
||||
elif value is not None:
|
||||
WINDOW.setProperty(property, value)
|
||||
else:
|
||||
return WINDOW.getProperty(property)
|
||||
|
||||
if type == "music":
|
||||
def settings(setting, value=None):
|
||||
# Get or add addon setting
|
||||
addon = xbmcaddon.Addon(id='plugin.video.emby')
|
||||
|
||||
if value is not None:
|
||||
addon.setSetting(setting, value)
|
||||
else:
|
||||
return addon.getSetting(setting)
|
||||
|
||||
def language(stringid):
|
||||
# Central string retrieval
|
||||
addon = xbmcaddon.Addon(id='plugin.video.emby')
|
||||
string = addon.getLocalizedString(stringid)
|
||||
|
||||
return string
|
||||
|
||||
def kodiSQL(type="video"):
|
||||
|
||||
if type == "emby":
|
||||
dbPath = xbmc.translatePath("special://database/emby.db").decode('utf-8')
|
||||
elif type == "music":
|
||||
dbPath = getKodiMusicDBPath()
|
||||
elif type == "texture":
|
||||
dbPath = xbmc.translatePath("special://database/Textures13.db").decode('utf-8')
|
||||
|
@ -95,218 +112,139 @@ def getKodiMusicDBPath():
|
|||
% dbVersion.get(kodibuild, "")).decode('utf-8')
|
||||
return dbPath
|
||||
|
||||
def prettifyXml(elem):
|
||||
rough_string = etree.tostring(elem, "utf-8")
|
||||
reparsed = minidom.parseString(rough_string)
|
||||
return reparsed.toprettyxml(indent="\t")
|
||||
def reset():
|
||||
|
||||
dialog = xbmcgui.Dialog()
|
||||
|
||||
resp = dialog.yesno("Warning", "Are you sure you want to reset your local Kodi database?")
|
||||
if resp == 0:
|
||||
return
|
||||
|
||||
# first stop any db sync
|
||||
window('emby_shouldStop', value="true")
|
||||
count = 10
|
||||
while window('emby_dbScan') == "true":
|
||||
logMsg("EMBY", "Sync is running, will retry: %s..." % count)
|
||||
count -= 1
|
||||
if count == 0:
|
||||
dialog.ok("Warning", "Could not stop the database from running. Try again.")
|
||||
return
|
||||
xbmc.sleep(1000)
|
||||
|
||||
# Clean up the playlists
|
||||
path = xbmc.translatePath("special://profile/playlists/video/").decode('utf-8')
|
||||
dirs, files = xbmcvfs.listdir(path)
|
||||
for file in files:
|
||||
if file.startswith('Emby'):
|
||||
xbmcvfs.delete("%s%s" % (path, file))
|
||||
|
||||
# Clean up the video nodes
|
||||
import shutil
|
||||
path = xbmc.translatePath("special://profile/library/video/").decode('utf-8')
|
||||
dirs, files = xbmcvfs.listdir(path)
|
||||
for dir in dirs:
|
||||
if dir.startswith('Emby'):
|
||||
shutil.rmtree("%s%s" % (path, dir))
|
||||
for file in files:
|
||||
if file.startswith('emby'):
|
||||
xbmcvfs.delete("%s%s" % (path, file))
|
||||
|
||||
# Wipe the kodi databases
|
||||
logMsg("EMBY", "Resetting the Kodi video database.")
|
||||
connection = kodiSQL('video')
|
||||
cursor = connection.cursor()
|
||||
cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"')
|
||||
rows = cursor.fetchall()
|
||||
for row in rows:
|
||||
tablename = row[0]
|
||||
if tablename != "version":
|
||||
cursor.execute("DELETE FROM " + tablename)
|
||||
connection.commit()
|
||||
cursor.close()
|
||||
|
||||
if settings('disableMusic') != "true":
|
||||
logMsg("EMBY", "Resetting the Kodi music database.")
|
||||
connection = kodiSQL('music')
|
||||
cursor = connection.cursor()
|
||||
cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"')
|
||||
rows = cursor.fetchall()
|
||||
for row in rows:
|
||||
tablename = row[0]
|
||||
if tablename != "version":
|
||||
cursor.execute("DELETE FROM " + tablename)
|
||||
connection.commit()
|
||||
cursor.close()
|
||||
|
||||
# Wipe the emby database
|
||||
logMsg("EMBY", "Resetting the Emby database.")
|
||||
connection = kodiSQL('emby')
|
||||
cursor = connection.cursor()
|
||||
cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"')
|
||||
rows = cursor.fetchall()
|
||||
for row in rows:
|
||||
tablename = row[0]
|
||||
if tablename != "version":
|
||||
cursor.execute("DELETE FROM " + tablename)
|
||||
connection.commit()
|
||||
cursor.close()
|
||||
|
||||
# reset the install run flag
|
||||
settings('SyncInstallRunDone', value="false")
|
||||
|
||||
# Remove emby info
|
||||
resp = dialog.yesno("Warning", "Reset all Emby Addon settings?")
|
||||
if resp == 1:
|
||||
# Delete the settings
|
||||
addon = xbmcaddon.Addon()
|
||||
addondir = xbmc.translatePath(addon.getAddonInfo('profile')).decode('utf-8')
|
||||
dataPath = "%ssettings.xml" % addondir
|
||||
xbmcvfs.delete(dataPath)
|
||||
logMsg("EMBY", "Deleting: settings.xml", 1)
|
||||
|
||||
dialog.ok(
|
||||
heading="Emby for Kodi",
|
||||
line1="Database reset has completed, Kodi will now restart to apply the changes.")
|
||||
xbmc.executebuiltin('RestartApp')
|
||||
|
||||
def startProfiling():
|
||||
|
||||
pr = cProfile.Profile()
|
||||
pr.enable()
|
||||
|
||||
return pr
|
||||
|
||||
def stopProfiling(pr, profileName):
|
||||
|
||||
pr.disable()
|
||||
ps = pstats.Stats(pr)
|
||||
|
||||
addondir = xbmc.translatePath(xbmcaddon.Addon(id='plugin.video.emby').getAddonInfo('profile'))
|
||||
profiles = xbmc.translatePath("%sprofiles/"
|
||||
% xbmcaddon.Addon().getAddonInfo('profile')).decode('utf-8')
|
||||
|
||||
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(profiles):
|
||||
# Create the profiles folder
|
||||
xbmcvfs.mkdir(profiles)
|
||||
|
||||
if not xbmcvfs.exists(tabFileNamepath):
|
||||
xbmcvfs.mkdir(tabFileNamepath)
|
||||
timestamp = time.strftime("%Y-%m-%d %H-%M-%S")
|
||||
profile = "%s%s_profile_(%s).tab" % (profiles, profileName, timestamp)
|
||||
|
||||
f = open(tabFileName, 'wb')
|
||||
f = open(profile, 'wb')
|
||||
f.write("NumbCalls\tTotalTime\tCumulativeTime\tFunctionName\tFileName\r\n")
|
||||
for (key, value) in ps.stats.items():
|
||||
(filename, count, func_name) = key
|
||||
(ccalls, ncalls, total_time, cumulative_time, callers) = value
|
||||
try:
|
||||
f.write(str(ncalls) + "\t" + "{:10.4f}".format(total_time) + "\t" + "{:10.4f}".format(cumulative_time) + "\t" + func_name + "\t" + filename + "\r\n")
|
||||
f.write(
|
||||
"%s\t%s\t%s\t%s\t%s\r\n"
|
||||
% (ncalls, "{:10.4f}".format(total_time),
|
||||
"{:10.4f}".format(cumulative_time), func_name, filename))
|
||||
except ValueError:
|
||||
f.write(str(ncalls) + "\t" + "{0}".format(total_time) + "\t" + "{0}".format(cumulative_time) + "\t" + func_name + "\t" + filename + "\r\n")
|
||||
f.write(
|
||||
"%s\t%s\t%s\t%s\t%s\r\n"
|
||||
% (ncalls, "{0}".format(total_time),
|
||||
"{0}".format(cumulative_time), func_name, filename))
|
||||
f.close()
|
||||
|
||||
def 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):
|
||||
# For video nodes
|
||||
text = text.replace(":", "")
|
||||
|
@ -327,99 +265,223 @@ def normalize_nodes(text):
|
|||
|
||||
return text
|
||||
|
||||
def reloadProfile():
|
||||
# Useful to reload the add-on without restarting Kodi.
|
||||
profile = xbmc.getInfoLabel('System.ProfileName')
|
||||
xbmc.executebuiltin("LoadProfile(%s)" % profile)
|
||||
def normalize_string(text):
|
||||
# For theme media, do not modify unless
|
||||
# modified in TV Tunes
|
||||
text = text.replace(":", "")
|
||||
text = text.replace("/", "-")
|
||||
text = text.replace("\\", "-")
|
||||
text = text.replace("<", "")
|
||||
text = text.replace(">", "")
|
||||
text = text.replace("*", "")
|
||||
text = text.replace("?", "")
|
||||
text = text.replace('|', "")
|
||||
text = text.strip()
|
||||
# Remove dots from the last character as windows can not have directories
|
||||
# with dots at the end
|
||||
text = text.rstrip('.')
|
||||
text = unicodedata.normalize('NFKD', unicode(text, 'utf-8')).encode('ascii', 'ignore')
|
||||
|
||||
return text
|
||||
|
||||
def indent(elem, level=0):
|
||||
# Prettify xml trees
|
||||
i = "\n" + level*" "
|
||||
if len(elem):
|
||||
if not elem.text or not elem.text.strip():
|
||||
elem.text = i + " "
|
||||
if not elem.tail or not elem.tail.strip():
|
||||
elem.tail = i
|
||||
for elem in elem:
|
||||
indent(elem, level+1)
|
||||
if not elem.tail or not elem.tail.strip():
|
||||
elem.tail = i
|
||||
else:
|
||||
if level and (not elem.tail or not elem.tail.strip()):
|
||||
elem.tail = i
|
||||
|
||||
def sourcesXML():
|
||||
# To make Master lock compatible
|
||||
path = xbmc.translatePath("special://profile/").decode('utf-8')
|
||||
xmlpath = "%ssources.xml" % path
|
||||
|
||||
try:
|
||||
xmlparse = etree.parse(xmlpath)
|
||||
except: # Document is blank or missing
|
||||
root = etree.Element('sources')
|
||||
else:
|
||||
root = xmlparse.getroot()
|
||||
|
||||
|
||||
def reset():
|
||||
video = root.find('video')
|
||||
if video is None:
|
||||
video = etree.SubElement(root, 'video')
|
||||
etree.SubElement(video, 'default', attrib={'pathversion': "1"})
|
||||
|
||||
WINDOW = xbmcgui.Window( 10000 )
|
||||
return_value = xbmcgui.Dialog().yesno("Warning", "Are you sure you want to reset your local Kodi database?")
|
||||
# Add elements
|
||||
for i in range(1, 3):
|
||||
|
||||
if return_value == 0:
|
||||
return
|
||||
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 = (
|
||||
|
||||
# Because the settings dialog could be open
|
||||
# it seems to override settings so we need to close it before we reset settings.
|
||||
xbmc.executebuiltin("Dialog.Close(all,true)")
|
||||
"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)
|
||||
|
||||
#cleanup video nodes
|
||||
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)
|
||||
def passwordsXML():
|
||||
|
||||
settings('SyncInstallRunDone', "false")
|
||||
# To add network credentials
|
||||
path = xbmc.translatePath("special://userdata/").decode('utf-8')
|
||||
xmlpath = "%spasswords.xml" % path
|
||||
|
||||
# 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()
|
||||
|
||||
|
||||
# reset the install run flag
|
||||
#settings('SyncInstallRunDone', "false")
|
||||
#WINDOW.setProperty("SyncInstallRunDone", "false")
|
||||
try:
|
||||
xmlparse = etree.parse(xmlpath)
|
||||
except: # Document is blank or missing
|
||||
root = etree.Element('passwords')
|
||||
else:
|
||||
root = xmlparse.getroot()
|
||||
|
||||
dialog = xbmcgui.Dialog()
|
||||
# Reload would work instead of restart since the add-on is a service.
|
||||
#dialog.ok('Emby Reset', 'Database reset has completed, Kodi will now restart to apply the changes.')
|
||||
#WINDOW.clearProperty("SyncDatabaseShouldStop")
|
||||
#reloadProfile()
|
||||
dialog.ok('Emby Reset', 'Database reset has completed, Kodi will now restart to apply the changes.')
|
||||
xbmc.executebuiltin("RestartApp")
|
||||
credentials = settings('networkCreds')
|
||||
if credentials:
|
||||
# Present user with options
|
||||
option = dialog.select("Modify/Remove network credentials", ["Modify", "Remove"])
|
||||
|
||||
if option < 0:
|
||||
# User cancelled dialog
|
||||
return
|
||||
|
||||
elif option == 1:
|
||||
# User selected remove
|
||||
iterator = root.getiterator('passwords')
|
||||
|
||||
for paths in iterator:
|
||||
for path in paths:
|
||||
if path.find('.//from').text == "smb://%s/" % credentials:
|
||||
paths.remove(path)
|
||||
logMsg("EMBY", "Successfully removed credentials for: %s"
|
||||
% credentials, 1)
|
||||
etree.ElementTree(root).write(xmlpath)
|
||||
break
|
||||
else:
|
||||
logMsg("EMBY", "Failed to find saved server: %s in passwords.xml" % credentials, 1)
|
||||
|
||||
settings('networkCreds', value="")
|
||||
xbmcgui.Dialog().notification(
|
||||
heading="Emby for Kodi",
|
||||
message="%s removed from passwords.xml!" % credentials,
|
||||
icon="special://home/addons/plugin.video.emby/icon.png",
|
||||
time=1000,
|
||||
sound=False)
|
||||
return
|
||||
|
||||
elif option == 0:
|
||||
# User selected to modify
|
||||
server = dialog.input("Modify the computer name or ip address", credentials)
|
||||
if not server:
|
||||
return
|
||||
else:
|
||||
# No credentials added
|
||||
dialog.ok(
|
||||
heading="Network credentials",
|
||||
line1= (
|
||||
"Input the server name or IP address as indicated in your emby library paths. "
|
||||
'For example, the server name: \\\\SERVER-PC\\path\\ is "SERVER-PC".'))
|
||||
server = dialog.input("Enter the server name or IP address", settings('ipaddress'))
|
||||
if not server:
|
||||
return
|
||||
|
||||
# Network username
|
||||
user = dialog.input("Enter the network username")
|
||||
if not user:
|
||||
return
|
||||
# Network password
|
||||
password = dialog.input(
|
||||
heading="Enter the network password",
|
||||
option=xbmcgui.ALPHANUM_HIDE_INPUT)
|
||||
if not password:
|
||||
return
|
||||
|
||||
# Add elements
|
||||
for path in root.findall('.//path'):
|
||||
if path.find('.//from').text.lower() == "smb://%s/" % server.lower():
|
||||
# Found the server, rewrite credentials
|
||||
path.find('.//to').text = "smb://%s:%s@%s/" % (user, password, server)
|
||||
break
|
||||
else:
|
||||
# Server not found, add it.
|
||||
path = etree.SubElement(root, 'path')
|
||||
etree.SubElement(path, 'from', attrib={'pathversion': "1"}).text = "smb://%s/" % server
|
||||
topath = "smb://%s:%s@%s/" % (user, password, server)
|
||||
etree.SubElement(path, 'to', attrib={'pathversion': "1"}).text = topath
|
||||
# Force Kodi to see the credentials without restarting
|
||||
xbmcvfs.exists(topath)
|
||||
|
||||
# Add credentials
|
||||
settings('networkCreds', value="%s" % server)
|
||||
logMsg("EMBY", "Added server: %s to passwords.xml" % server, 1)
|
||||
# Prettify and write to file
|
||||
try:
|
||||
indent(root)
|
||||
except: pass
|
||||
etree.ElementTree(root).write(xmlpath)
|
||||
|
||||
dialog.notification(
|
||||
heading="Emby for Kodi",
|
||||
message="%s added to passwords.xml!" % server,
|
||||
icon="special://home/addons/plugin.video.emby/icon.png",
|
||||
time=1000,
|
||||
sound=False)
|
||||
|
||||
def playlistXSP(mediatype, tagname, viewtype="", delete=False):
|
||||
# Tagname is in unicode - actions: add or delete
|
||||
tagname = tagname.encode('utf-8')
|
||||
cleantagname = normalize_nodes(tagname)
|
||||
path = xbmc.translatePath("special://profile/playlists/video/").decode('utf-8')
|
||||
if viewtype == "mixed":
|
||||
plname = "%s - %s" % (tagname, mediatype)
|
||||
xsppath = "%sEmby %s - %s.xsp" % (path, cleantagname, mediatype)
|
||||
else:
|
||||
plname = tagname
|
||||
xsppath = "%sEmby %s.xsp" % (path, cleantagname)
|
||||
|
||||
# Create the playlist directory
|
||||
if not xbmcvfs.exists(path):
|
||||
xbmcvfs.mkdirs(path)
|
||||
|
||||
# Only add the playlist if it doesn't already exists
|
||||
if xbmcvfs.exists(xsppath):
|
||||
|
||||
if delete:
|
||||
xbmcvfs.delete(xsppath)
|
||||
logMsg("EMBY", "Successfully removed playlist: %s." % tagname, 1)
|
||||
|
||||
return
|
||||
|
||||
# Using write process since there's no guarantee the xml declaration works with etree
|
||||
itemtypes = {
|
||||
'homevideos': "movies"
|
||||
}
|
||||
f = open(xsppath, 'w')
|
||||
f.write(
|
||||
'<?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)
|
|
@ -1,466 +1,344 @@
|
|||
#################################################################################################
|
||||
# VideoNodes - utils to create video nodes listings in kodi for the emby addon
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
#################################################################################################
|
||||
|
||||
|
||||
import xbmc
|
||||
import xbmcgui
|
||||
import xbmcaddon
|
||||
import xbmcvfs
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
#import common elementree because cElementree has issues with kodi
|
||||
import xml.etree.ElementTree as etree
|
||||
|
||||
import Utils as utils
|
||||
import xbmc
|
||||
import xbmcaddon
|
||||
import xbmcvfs
|
||||
|
||||
from ReadEmbyDB import ReadEmbyDB
|
||||
WINDOW = xbmcgui.Window(10000)
|
||||
import clientinfo
|
||||
import utils
|
||||
|
||||
addonSettings = xbmcaddon.Addon()
|
||||
language = addonSettings.getLocalizedString
|
||||
|
||||
class VideoNodes():
|
||||
#################################################################################################
|
||||
|
||||
|
||||
def buildVideoNodeForView(self, tagname, type, windowPropId):
|
||||
#this method will build a video node for a particular Emby view (= tag in kodi)
|
||||
#we set some window props here to for easy future reference and to be used in skins (for easy access only)
|
||||
tagname_normalized = utils.normalize_nodes(tagname.encode('utf-8'))
|
||||
class VideoNodes(object):
|
||||
|
||||
libraryPath = xbmc.translatePath("special://profile/library/video/Emby - %s/" %tagname_normalized)
|
||||
kodiVersion = 14
|
||||
if xbmc.getInfoLabel("System.BuildVersion").startswith("15") or xbmc.getInfoLabel("System.BuildVersion").startswith("16"):
|
||||
kodiVersion = 15
|
||||
|
||||
#create tag node - index
|
||||
xbmcvfs.mkdir(libraryPath)
|
||||
nodefile = os.path.join(libraryPath, "index.xml")
|
||||
root = etree.Element("node", {"order":"0"})
|
||||
etree.SubElement(root, "label").text = tagname
|
||||
etree.SubElement(root, "icon").text = "special://home/addons/plugin.video.emby/icon.png"
|
||||
path = "library://video/Emby - %s/" %tagname_normalized
|
||||
WINDOW.setProperty("Emby.nodes.%s.index" %str(windowPropId),path)
|
||||
try:
|
||||
etree.ElementTree(root).write(nodefile, xml_declaration=True)
|
||||
except:
|
||||
etree.ElementTree(root).write(nodefile)
|
||||
def __init__(self):
|
||||
|
||||
#create tag node - all items
|
||||
nodefile = os.path.join(libraryPath, tagname_normalized + "_all.xml")
|
||||
root = etree.Element("node", {"order":"1", "type":"filter"})
|
||||
etree.SubElement(root, "label").text = tagname
|
||||
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"
|
||||
etree.SubElement(root, "order", {"direction":"ascending"}).text = "sorttitle"
|
||||
Rule = etree.SubElement(root, "rule", {"field":"tag","operator":"is"})
|
||||
WINDOW.setProperty("Emby.nodes.%s.title" %str(windowPropId),tagname)
|
||||
path = "library://video/Emby - %s/%s_all.xml"%(tagname_normalized,tagname_normalized)
|
||||
WINDOW.setProperty("Emby.nodes.%s.path" %str(windowPropId),"ActivateWindow(Video,%s,return)"%path)
|
||||
WINDOW.setProperty("Emby.nodes.%s.content" %str(windowPropId),path)
|
||||
WINDOW.setProperty("Emby.nodes.%s.type" %str(windowPropId),type)
|
||||
etree.SubElement(Rule, "value").text = tagname
|
||||
try:
|
||||
etree.ElementTree(root).write(nodefile, xml_declaration=True)
|
||||
except:
|
||||
etree.ElementTree(root).write(nodefile)
|
||||
clientInfo = clientinfo.ClientInfo()
|
||||
self.addonName = clientInfo.getAddonName()
|
||||
|
||||
#create tag node - recent items
|
||||
nodefile = os.path.join(libraryPath, tagname_normalized + "_recent.xml")
|
||||
root = etree.Element("node", {"order":"2", "type":"filter"})
|
||||
if type == "tvshows":
|
||||
label = language(30170)
|
||||
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:
|
||||
label = language(30174)
|
||||
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
|
||||
etree.SubElement(root, "order", {"direction":"descending"}).text = "dateadded"
|
||||
#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.recent.title" %str(windowPropId),label)
|
||||
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)
|
||||
WINDOW.setProperty("Emby.nodes.%s.recent.content" %str(windowPropId),path)
|
||||
try:
|
||||
etree.ElementTree(root).write(nodefile, xml_declaration=True)
|
||||
except:
|
||||
etree.ElementTree(root).write(nodefile)
|
||||
# Folder
|
||||
root = etree.Element('node', attrib={'order': "%s" % order, 'type': "folder"})
|
||||
|
||||
#create tag node - inprogress items
|
||||
nodefile = os.path.join(libraryPath, tagname_normalized + "_progress.xml")
|
||||
root = etree.Element("node", {"order":"3", "type":"filter"})
|
||||
if type == "tvshows":
|
||||
label = language(30171)
|
||||
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:
|
||||
label = language(30177)
|
||||
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
|
||||
#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.inprogress.title" %str(windowPropId),label)
|
||||
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)
|
||||
dirname = cleantagname
|
||||
|
||||
path = xbmc.translatePath("special://profile/library/video/").decode('utf-8')
|
||||
nodepath = xbmc.translatePath(
|
||||
"special://profile/library/video/Emby - %s/" % dirname).decode('utf-8')
|
||||
|
||||
# Verify the video directory
|
||||
if not xbmcvfs.exists(path):
|
||||
shutil.copytree(
|
||||
src=xbmc.translatePath("special://xbmc/system/library/video/").decode('utf-8'),
|
||||
dst=xbmc.translatePath("special://profile/library/video/").decode('utf-8'))
|
||||
xbmcvfs.exists(path)
|
||||
|
||||
# Create the node directory
|
||||
if not xbmcvfs.exists(nodepath):
|
||||
# We need to copy over the default items
|
||||
xbmcvfs.mkdirs(nodepath)
|
||||
else:
|
||||
if delete:
|
||||
dirs, files = xbmcvfs.listdir(nodepath)
|
||||
for file in files:
|
||||
xbmcvfs.delete(nodepath + file)
|
||||
|
||||
self.logMsg("Sucessfully removed videonode: %s." % tagname, 1)
|
||||
return
|
||||
|
||||
# Create index entry
|
||||
nodeXML = "%sindex.xml" % nodepath
|
||||
# Set windows property
|
||||
path = "library://video/Emby - %s/" % dirname
|
||||
for i in range(1, indexnumber):
|
||||
# Verify to make sure we don't create duplicates
|
||||
if utils.window('Emby.nodes.%s.index' % i) == path:
|
||||
return
|
||||
|
||||
utils.window('Emby.nodes.%s.index' % indexnumber, value=path)
|
||||
# Root
|
||||
root = self.commonRoot(order=0, label=dirname, tagname=tagname, roottype=0)
|
||||
try:
|
||||
etree.ElementTree(root).write(nodefile, xml_declaration=True)
|
||||
except:
|
||||
etree.ElementTree(root).write(nodefile)
|
||||
utils.indent(root)
|
||||
except: pass
|
||||
etree.ElementTree(root).write(nodeXML)
|
||||
|
||||
#some movies-only nodes
|
||||
if type == "movies":
|
||||
|
||||
#unwatched movies
|
||||
nodefile = os.path.join(libraryPath, tagname_normalized + "_unwatched.xml")
|
||||
root = etree.Element("node", {"order":"4", "type":"filter"})
|
||||
label = language(30189)
|
||||
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"
|
||||
Rule = etree.SubElement(root, "rule", {"field":"tag","operator":"is"})
|
||||
etree.SubElement(Rule, "value").text = tagname
|
||||
Rule = etree.SubElement(root, "rule", {"field":"playcount","operator":"is"})
|
||||
etree.SubElement(Rule, "value").text = "0"
|
||||
etree.SubElement(root, "order", {"direction":"ascending"}).text = "sorttitle"
|
||||
Rule2 = etree.SubElement(root, "rule", {"field":"playcount","operator":"is"})
|
||||
etree.SubElement(Rule2, "value").text = "0"
|
||||
WINDOW.setProperty("Emby.nodes.%s.unwatched.title" %str(windowPropId),label)
|
||||
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)
|
||||
WINDOW.setProperty("Emby.nodes.%s.unwatched.content" %str(windowPropId),path)
|
||||
nodetypes = {
|
||||
|
||||
'1': "all",
|
||||
'2': "recent",
|
||||
'3': "recentepisodes",
|
||||
'4': "inprogress",
|
||||
'5': "inprogressepisodes",
|
||||
'6': "unwatched",
|
||||
'7': "nextupepisodes",
|
||||
'8': "sets",
|
||||
'9': "genres",
|
||||
'10': "random",
|
||||
'11': "recommended"
|
||||
}
|
||||
mediatypes = {
|
||||
# label according to nodetype per mediatype
|
||||
'movies': {
|
||||
'1': tagname,
|
||||
'2': 30174,
|
||||
'4': 30177,
|
||||
'6': 30189,
|
||||
'8': 20434,
|
||||
'9': 135,
|
||||
'10': 30229,
|
||||
'11': 30230},
|
||||
|
||||
'tvshows': {
|
||||
'1': tagname,
|
||||
'2': 30170,
|
||||
'3': 30175,
|
||||
'4': 30171,
|
||||
'5': 30178,
|
||||
'7': 30179,
|
||||
'9': 135,
|
||||
'10': 30229,
|
||||
'11': 30230},
|
||||
}
|
||||
|
||||
nodes = mediatypes[mediatype]
|
||||
for node in nodes:
|
||||
|
||||
nodetype = nodetypes[node]
|
||||
nodeXML = "%s%s_%s.xml" % (nodepath, cleantagname, nodetype)
|
||||
# Get label
|
||||
stringid = nodes[node]
|
||||
if node != '1':
|
||||
label = utils.language(stringid)
|
||||
if not label:
|
||||
label = xbmc.getLocalizedString(stringid)
|
||||
else:
|
||||
label = stringid
|
||||
|
||||
# Set window properties
|
||||
if nodetype == "nextupepisodes":
|
||||
# Custom query
|
||||
path = "plugin://plugin.video.emby/?id=%s&mode=nextup&limit=25" % tagname
|
||||
elif kodiversion == 14 and nodetype == "recentepisodes":
|
||||
# Custom query
|
||||
path = "plugin://plugin.video.emby/?id=%s&mode=recentepisodes&limit=25" % tagname
|
||||
elif kodiversion == 14 and nodetype == "inprogressepisodes":
|
||||
# Custom query
|
||||
path = "plugin://plugin.video.emby/?id=%s&mode=inprogressepisodes&limit=25"% tagname
|
||||
else:
|
||||
path = "library://video/Emby - %s/%s_%s.xml" % (dirname, cleantagname, nodetype)
|
||||
windowpath = "ActivateWindow(Video, %s, return)" % path
|
||||
|
||||
if nodetype == "all":
|
||||
|
||||
if viewtype == "mixed":
|
||||
templabel = dirname
|
||||
else:
|
||||
templabel = label
|
||||
|
||||
embynode = "Emby.nodes.%s" % indexnumber
|
||||
utils.window('%s.title' % embynode, value=templabel)
|
||||
utils.window('%s.path' % embynode, value=windowpath)
|
||||
utils.window('%s.content' % embynode, value=path)
|
||||
utils.window('%s.type' % embynode, value=mediatype)
|
||||
else:
|
||||
embynode = "Emby.nodes.%s.%s" % (indexnumber, nodetype)
|
||||
utils.window('%s.title' % embynode, value=label)
|
||||
utils.window('%s.path' % embynode, value=windowpath)
|
||||
utils.window('%s.content' % embynode, value=path)
|
||||
|
||||
if xbmcvfs.exists(nodeXML):
|
||||
# Don't recreate xml if already exists
|
||||
continue
|
||||
|
||||
|
||||
# Create the root
|
||||
if nodetype == "nextupepisodes" or (kodiversion == 14 and
|
||||
nodetype in ('recentepisodes', 'inprogressepisodes')):
|
||||
# Folder type with plugin path
|
||||
root = self.commonRoot(order=node, label=label, tagname=tagname, roottype=2)
|
||||
etree.SubElement(root, 'path').text = path
|
||||
etree.SubElement(root, 'content').text = "episodes"
|
||||
else:
|
||||
root = self.commonRoot(order=node, label=label, tagname=tagname)
|
||||
if nodetype in ('recentepisodes', 'inprogressepisodes'):
|
||||
etree.SubElement(root, 'content').text = "episodes"
|
||||
else:
|
||||
etree.SubElement(root, 'content').text = mediatype
|
||||
|
||||
limit = "25"
|
||||
# Elements per nodetype
|
||||
if nodetype == "all":
|
||||
etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle"
|
||||
|
||||
elif nodetype == "recent":
|
||||
etree.SubElement(root, 'order', {'direction': "descending"}).text = "dateadded"
|
||||
etree.SubElement(root, 'limit').text = limit
|
||||
rule = etree.SubElement(root, 'rule', {'field': "playcount", 'operator': "is"})
|
||||
etree.SubElement(rule, 'value').text = "0"
|
||||
|
||||
elif nodetype == "inprogress":
|
||||
etree.SubElement(root, 'rule', {'field': "inprogress", 'operator': "true"})
|
||||
etree.SubElement(root, 'limit').text = limit
|
||||
|
||||
elif nodetype == "genres":
|
||||
etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle"
|
||||
etree.SubElement(root, 'group').text = "genres"
|
||||
|
||||
elif nodetype == "unwatched":
|
||||
etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle"
|
||||
rule = etree.SubElement(root, "rule", {'field': "playcount", 'operator': "is"})
|
||||
etree.SubElement(rule, 'value').text = "0"
|
||||
|
||||
elif nodetype == "sets":
|
||||
etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle"
|
||||
etree.SubElement(root, 'group').text = "sets"
|
||||
|
||||
elif nodetype == "random":
|
||||
etree.SubElement(root, 'order', {'direction': "ascending"}).text = "random"
|
||||
etree.SubElement(root, 'limit').text = limit
|
||||
|
||||
elif nodetype == "recommended":
|
||||
etree.SubElement(root, 'order', {'direction': "descending"}).text = "rating"
|
||||
etree.SubElement(root, 'limit').text = limit
|
||||
rule = etree.SubElement(root, 'rule', {'field': "playcount", 'operator': "is"})
|
||||
etree.SubElement(rule, 'value').text = "0"
|
||||
rule2 = etree.SubElement(root, 'rule',
|
||||
attrib={'field': "rating", 'operator': "greaterthan"})
|
||||
etree.SubElement(rule2, 'value').text = "7"
|
||||
|
||||
elif nodetype == "recentepisodes":
|
||||
# Kodi Isengard, Jarvis
|
||||
etree.SubElement(root, 'order', {'direction': "descending"}).text = "dateadded"
|
||||
etree.SubElement(root, 'limit').text = limit
|
||||
rule = etree.SubElement(root, 'rule', {'field': "playcount", 'operator': "is"})
|
||||
etree.SubElement(rule, 'value').text = "0"
|
||||
|
||||
elif nodetype == "inprogressepisodes":
|
||||
# Kodi Isengard, Jarvis
|
||||
etree.SubElement(root, 'limit').text = "25"
|
||||
rule = etree.SubElement(root, 'rule',
|
||||
attrib={'field': "inprogress", 'operator':"true"})
|
||||
|
||||
try:
|
||||
etree.ElementTree(root).write(nodefile, xml_declaration=True)
|
||||
except:
|
||||
etree.ElementTree(root).write(nodefile)
|
||||
utils.indent(root)
|
||||
except: pass
|
||||
etree.ElementTree(root).write(nodeXML)
|
||||
|
||||
#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")
|
||||
root = etree.Element("node", {"order":"9", "type":"filter"})
|
||||
label = xbmc.getLocalizedString(135)
|
||||
etree.SubElement(root, "label").text = label
|
||||
etree.SubElement(root, "match").text = "all"
|
||||
etree.SubElement(root, "group").text = "genres"
|
||||
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)
|
||||
tagname = tagname.encode('utf-8')
|
||||
cleantagname = utils.normalize_nodes(tagname)
|
||||
nodepath = xbmc.translatePath("special://profile/library/video/").decode('utf-8')
|
||||
nodeXML = "%semby_%s.xml" % (nodepath, cleantagname)
|
||||
path = "library://video/emby_%s.xml" % (cleantagname)
|
||||
windowpath = "ActivateWindow(Video, %s, return)" % path
|
||||
|
||||
# Create the video node directory
|
||||
if not xbmcvfs.exists(nodepath):
|
||||
# We need to copy over the default items
|
||||
shutil.copytree(
|
||||
src=xbmc.translatePath("special://xbmc/system/library/video").decode('utf-8'),
|
||||
dst=xbmc.translatePath("special://profile/library/video").decode('utf-8'))
|
||||
xbmcvfs.exists(path)
|
||||
|
||||
labels = {
|
||||
|
||||
'Favorite movies': 30180,
|
||||
'Favorite tvshows': 30181,
|
||||
'channels': 30173
|
||||
}
|
||||
label = utils.language(labels[tagname])
|
||||
embynode = "Emby.nodes.%s" % indexnumber
|
||||
utils.window('%s.title' % embynode, value=label)
|
||||
utils.window('%s.path' % embynode, value=windowpath)
|
||||
utils.window('%s.content' % embynode, value=path)
|
||||
utils.window('%s.type' % embynode, value=itemtype)
|
||||
|
||||
if xbmcvfs.exists(nodeXML):
|
||||
# Don't recreate xml if already exists
|
||||
return
|
||||
|
||||
if itemtype == "channels":
|
||||
root = self.commonRoot(order=1, label=label, tagname=tagname, roottype=2)
|
||||
etree.SubElement(root, 'path').text = "plugin://plugin.video.emby/?id=0&mode=channels"
|
||||
else:
|
||||
root = self.commonRoot(order=1, label=label, tagname=tagname)
|
||||
etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle"
|
||||
|
||||
etree.SubElement(root, 'content').text = mediatype
|
||||
|
||||
try:
|
||||
etree.ElementTree(root).write(nodefile, xml_declaration=True)
|
||||
except:
|
||||
etree.ElementTree(root).write(nodefile)
|
||||
utils.indent(root)
|
||||
except: pass
|
||||
etree.ElementTree(root).write(nodeXML)
|
||||
|
||||
#create tag node - random items
|
||||
nodefile = os.path.join(libraryPath, tagname_normalized + "_random.xml")
|
||||
root = etree.Element("node", {"order":"10", "type":"filter"})
|
||||
label = language(30229)
|
||||
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
|
||||
#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)
|
||||
def clearProperties(self):
|
||||
|
||||
#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)
|
||||
self.logMsg("Clearing nodes properties.", 1)
|
||||
embyprops = utils.window('Emby.nodes.total')
|
||||
propnames = [
|
||||
|
||||
#### TAGS ONLY FOR TV SHOWS COLLECTIONS ####
|
||||
if type == "tvshows":
|
||||
"index","path","title","content",
|
||||
"inprogress.content","inprogress.title",
|
||||
"inprogress.content","inprogress.path",
|
||||
"nextepisodes.title","nextepisodes.content",
|
||||
"nextepisodes.path","unwatched.title",
|
||||
"unwatched.content","unwatched.path",
|
||||
"recent.title","recent.content","recent.path",
|
||||
"recentepisodes.title","recentepisodes.content",
|
||||
"recentepisodes.path","inprogressepisodes.title",
|
||||
"inprogressepisodes.content","inprogressepisodes.path"
|
||||
]
|
||||
|
||||
#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
|
||||
|
||||
#create tag node for emby channels
|
||||
nodefile = os.path.join(xbmc.translatePath("special://profile/library/video"), "emby_channels.xml")
|
||||
root = etree.Element("node", {"order":"1", "type":"folder"})
|
||||
label = language(30173)
|
||||
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
|
||||
if embyprops:
|
||||
totalnodes = int(embyprops)
|
||||
for i in range(totalnodes):
|
||||
for prop in propnames:
|
||||
utils.window('Emby.nodes.%s.%s' % (str(i), prop), clear=True)
|
Loading…
Reference in a new issue