Remove to readd
This commit is contained in:
parent
08f9add813
commit
9855ac4c94
11 changed files with 0 additions and 5377 deletions
|
@ -1,378 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
##################################################################################################
|
|
||||||
|
|
||||||
import clientinfo
|
|
||||||
import utils
|
|
||||||
|
|
||||||
##################################################################################################
|
|
||||||
|
|
||||||
|
|
||||||
class API():
|
|
||||||
|
|
||||||
def __init__(self, item):
|
|
||||||
|
|
||||||
self.item = item
|
|
||||||
self.clientinfo = clientinfo.ClientInfo()
|
|
||||||
self.addonName = self.clientinfo.getAddonName()
|
|
||||||
|
|
||||||
def logMsg(self, msg, lvl=1):
|
|
||||||
|
|
||||||
className = self.__class__.__name__
|
|
||||||
utils.logMsg("%s %s" % (self.addonName, className), msg, lvl)
|
|
||||||
|
|
||||||
|
|
||||||
def getUserData(self):
|
|
||||||
# Default
|
|
||||||
favorite = False
|
|
||||||
playcount = None
|
|
||||||
played = False
|
|
||||||
lastPlayedDate = None
|
|
||||||
resume = 0
|
|
||||||
rating = 0
|
|
||||||
|
|
||||||
try:
|
|
||||||
userdata = self.item['UserData']
|
|
||||||
|
|
||||||
except KeyError: # No userdata found.
|
|
||||||
pass
|
|
||||||
|
|
||||||
else:
|
|
||||||
favorite = userdata['IsFavorite']
|
|
||||||
likes = userdata.get('Likes')
|
|
||||||
# Rating for album and songs
|
|
||||||
if favorite:
|
|
||||||
rating = 5
|
|
||||||
elif likes:
|
|
||||||
rating = 3
|
|
||||||
elif likes == False:
|
|
||||||
rating = 1
|
|
||||||
else:
|
|
||||||
rating = 0
|
|
||||||
|
|
||||||
lastPlayedDate = userdata.get('LastPlayedDate')
|
|
||||||
if lastPlayedDate:
|
|
||||||
lastPlayedDate = lastPlayedDate.split('.')[0].replace('T', " ")
|
|
||||||
|
|
||||||
if userdata['Played']:
|
|
||||||
# Playcount is tied to the watch status
|
|
||||||
played = True
|
|
||||||
playcount = userdata['PlayCount']
|
|
||||||
if playcount == 0:
|
|
||||||
playcount = 1
|
|
||||||
|
|
||||||
if lastPlayedDate is None:
|
|
||||||
lastPlayedDate = self.getDateCreated()
|
|
||||||
|
|
||||||
playbackPosition = userdata.get('PlaybackPositionTicks')
|
|
||||||
if playbackPosition:
|
|
||||||
resume = playbackPosition / 10000000.0
|
|
||||||
|
|
||||||
return {
|
|
||||||
|
|
||||||
'Favorite': favorite,
|
|
||||||
'PlayCount': playcount,
|
|
||||||
'Played': played,
|
|
||||||
'LastPlayedDate': lastPlayedDate,
|
|
||||||
'Resume': resume,
|
|
||||||
'Rating': rating
|
|
||||||
}
|
|
||||||
|
|
||||||
def getPeople(self):
|
|
||||||
# Process People
|
|
||||||
director = []
|
|
||||||
writer = []
|
|
||||||
cast = []
|
|
||||||
|
|
||||||
try:
|
|
||||||
people = self.item['People']
|
|
||||||
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
else:
|
|
||||||
for person in people:
|
|
||||||
|
|
||||||
type = person['Type']
|
|
||||||
name = person['Name']
|
|
||||||
|
|
||||||
if "Director" in type:
|
|
||||||
director.append(name)
|
|
||||||
elif "Actor" in type:
|
|
||||||
cast.append(name)
|
|
||||||
elif type in ("Writing", "Writer"):
|
|
||||||
writer.append(name)
|
|
||||||
|
|
||||||
return {
|
|
||||||
|
|
||||||
'Director': director,
|
|
||||||
'Writer': writer,
|
|
||||||
'Cast': cast
|
|
||||||
}
|
|
||||||
|
|
||||||
def getMediaStreams(self):
|
|
||||||
item = self.item
|
|
||||||
videotracks = []
|
|
||||||
audiotracks = []
|
|
||||||
subtitlelanguages = []
|
|
||||||
|
|
||||||
try:
|
|
||||||
media_streams = item['MediaSources'][0]['MediaStreams']
|
|
||||||
|
|
||||||
except KeyError:
|
|
||||||
media_streams = item['MediaStreams']
|
|
||||||
|
|
||||||
for media_stream in media_streams:
|
|
||||||
# Sort through Video, Audio, Subtitle
|
|
||||||
stream_type = media_stream['Type']
|
|
||||||
codec = media_stream.get('Codec', "").lower()
|
|
||||||
profile = media_stream.get('Profile', "").lower()
|
|
||||||
|
|
||||||
if stream_type == "Video":
|
|
||||||
# Height, Width, Codec, AspectRatio, AspectFloat, 3D
|
|
||||||
track = {
|
|
||||||
|
|
||||||
'videocodec': codec,
|
|
||||||
'height': media_stream.get('Height'),
|
|
||||||
'width': media_stream.get('Width'),
|
|
||||||
'video3DFormat': item.get('Video3DFormat'),
|
|
||||||
'aspectratio': 1.85
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
container = item['MediaSources'][0]['Container'].lower()
|
|
||||||
except:
|
|
||||||
container = ""
|
|
||||||
|
|
||||||
# Sort codec vs container/profile
|
|
||||||
if "msmpeg4" in codec:
|
|
||||||
track['videocodec'] = "divx"
|
|
||||||
elif "mpeg4" in codec:
|
|
||||||
if "simple profile" in profile or not profile:
|
|
||||||
track['videocodec'] = "xvid"
|
|
||||||
elif "h264" in codec:
|
|
||||||
if container in ("mp4", "mov", "m4v"):
|
|
||||||
track['videocodec'] = "avc1"
|
|
||||||
|
|
||||||
# Aspect ratio
|
|
||||||
if item.get('AspectRatio'):
|
|
||||||
# Metadata AR
|
|
||||||
aspectratio = item['AspectRatio']
|
|
||||||
else: # File AR
|
|
||||||
aspectratio = media_stream.get('AspectRatio', "0")
|
|
||||||
|
|
||||||
try:
|
|
||||||
aspectwidth, aspectheight = aspectratio.split(':')
|
|
||||||
track['aspectratio'] = round(float(aspectwidth) / float(aspectheight), 6)
|
|
||||||
|
|
||||||
except ValueError:
|
|
||||||
width = track['width']
|
|
||||||
height = track['height']
|
|
||||||
|
|
||||||
if width and height:
|
|
||||||
track['aspectratio'] = round(float(width / height), 6)
|
|
||||||
|
|
||||||
videotracks.append(track)
|
|
||||||
|
|
||||||
elif stream_type == "Audio":
|
|
||||||
# Codec, Channels, language
|
|
||||||
track = {
|
|
||||||
|
|
||||||
'audiocodec': codec,
|
|
||||||
'channels': media_stream.get('Channels'),
|
|
||||||
'audiolanguage': media_stream.get('Language')
|
|
||||||
}
|
|
||||||
|
|
||||||
if "dca" in codec and "dts-hd ma" in profile:
|
|
||||||
track['audiocodec'] = "dtshd_ma"
|
|
||||||
|
|
||||||
audiotracks.append(track)
|
|
||||||
|
|
||||||
elif stream_type == "Subtitle":
|
|
||||||
# Language
|
|
||||||
subtitlelanguages.append(media_stream.get('Language', "Unknown"))
|
|
||||||
|
|
||||||
return {
|
|
||||||
|
|
||||||
'video': videotracks,
|
|
||||||
'audio': audiotracks,
|
|
||||||
'subtitle': subtitlelanguages
|
|
||||||
}
|
|
||||||
|
|
||||||
def getRuntime(self):
|
|
||||||
item = self.item
|
|
||||||
try:
|
|
||||||
runtime = item['RunTimeTicks'] / 10000000.0
|
|
||||||
|
|
||||||
except KeyError:
|
|
||||||
runtime = item.get('CumulativeRunTimeTicks', 0) / 10000000.0
|
|
||||||
|
|
||||||
return runtime
|
|
||||||
|
|
||||||
def adjustResume(self, resume_seconds):
|
|
||||||
|
|
||||||
resume = 0
|
|
||||||
if resume_seconds:
|
|
||||||
resume = round(float(resume_seconds), 6)
|
|
||||||
jumpback = int(utils.settings('resumeJumpBack'))
|
|
||||||
if resume > jumpback:
|
|
||||||
# To avoid negative bookmark
|
|
||||||
resume = resume - jumpback
|
|
||||||
|
|
||||||
return resume
|
|
||||||
|
|
||||||
def getStudios(self):
|
|
||||||
# Process Studios
|
|
||||||
item = self.item
|
|
||||||
studios = []
|
|
||||||
|
|
||||||
try:
|
|
||||||
studio = item['SeriesStudio']
|
|
||||||
studios.append(self.verifyStudio(studio))
|
|
||||||
|
|
||||||
except KeyError:
|
|
||||||
studioList = item['Studios']
|
|
||||||
for studio in studioList:
|
|
||||||
|
|
||||||
name = studio['Name']
|
|
||||||
studios.append(self.verifyStudio(name))
|
|
||||||
|
|
||||||
return studios
|
|
||||||
|
|
||||||
def verifyStudio(self, studioName):
|
|
||||||
# Convert studio for Kodi to properly detect them
|
|
||||||
studios = {
|
|
||||||
|
|
||||||
'abc (us)': "ABC",
|
|
||||||
'fox (us)': "FOX",
|
|
||||||
'mtv (us)': "MTV",
|
|
||||||
'showcase (ca)': "Showcase",
|
|
||||||
'wgn america': "WGN"
|
|
||||||
}
|
|
||||||
|
|
||||||
return studios.get(studioName.lower(), studioName)
|
|
||||||
|
|
||||||
def getChecksum(self):
|
|
||||||
# Use the etags checksum and userdata
|
|
||||||
item = self.item
|
|
||||||
userdata = item['UserData']
|
|
||||||
|
|
||||||
checksum = "%s%s%s%s%s%s" % (
|
|
||||||
|
|
||||||
item['Etag'],
|
|
||||||
userdata['Played'],
|
|
||||||
userdata['IsFavorite'],
|
|
||||||
userdata['PlaybackPositionTicks'],
|
|
||||||
userdata.get('UnplayedItemCount', ""),
|
|
||||||
userdata.get('LastPlayedDate', "")
|
|
||||||
)
|
|
||||||
|
|
||||||
return checksum
|
|
||||||
|
|
||||||
def getGenres(self):
|
|
||||||
item = self.item
|
|
||||||
all_genres = ""
|
|
||||||
genres = item.get('Genres', item.get('SeriesGenres'))
|
|
||||||
|
|
||||||
if genres:
|
|
||||||
all_genres = " / ".join(genres)
|
|
||||||
|
|
||||||
return all_genres
|
|
||||||
|
|
||||||
def getDateCreated(self):
|
|
||||||
|
|
||||||
try:
|
|
||||||
dateadded = self.item['DateCreated']
|
|
||||||
dateadded = dateadded.split('.')[0].replace('T', " ")
|
|
||||||
except KeyError:
|
|
||||||
dateadded = None
|
|
||||||
|
|
||||||
return dateadded
|
|
||||||
|
|
||||||
def getPremiereDate(self):
|
|
||||||
|
|
||||||
try:
|
|
||||||
premiere = self.item['PremiereDate']
|
|
||||||
premiere = premiere.split('.')[0].replace('T', " ")
|
|
||||||
except KeyError:
|
|
||||||
premiere = None
|
|
||||||
|
|
||||||
return premiere
|
|
||||||
|
|
||||||
def getOverview(self):
|
|
||||||
|
|
||||||
try:
|
|
||||||
overview = self.item['Overview']
|
|
||||||
overview = overview.replace("\"", "\'")
|
|
||||||
overview = overview.replace("\n", " ")
|
|
||||||
overview = overview.replace("\r", " ")
|
|
||||||
except KeyError:
|
|
||||||
overview = ""
|
|
||||||
|
|
||||||
return overview
|
|
||||||
|
|
||||||
def getTagline(self):
|
|
||||||
|
|
||||||
try:
|
|
||||||
tagline = self.item['Taglines'][0]
|
|
||||||
except IndexError:
|
|
||||||
tagline = None
|
|
||||||
|
|
||||||
return tagline
|
|
||||||
|
|
||||||
def getProvider(self, providername):
|
|
||||||
|
|
||||||
try:
|
|
||||||
provider = self.item['ProviderIds'][providername]
|
|
||||||
except KeyError:
|
|
||||||
provider = None
|
|
||||||
|
|
||||||
return provider
|
|
||||||
|
|
||||||
def getMpaa(self):
|
|
||||||
# Convert more complex cases
|
|
||||||
mpaa = self.item.get('OfficialRating', "")
|
|
||||||
|
|
||||||
if mpaa in ("NR", "UR"):
|
|
||||||
# Kodi seems to not like NR, but will accept Not Rated
|
|
||||||
mpaa = "Not Rated"
|
|
||||||
|
|
||||||
return mpaa
|
|
||||||
|
|
||||||
def getCountry(self):
|
|
||||||
|
|
||||||
try:
|
|
||||||
country = self.item['ProductionLocations'][0]
|
|
||||||
except IndexError:
|
|
||||||
country = None
|
|
||||||
|
|
||||||
return country
|
|
||||||
|
|
||||||
def getFilePath(self):
|
|
||||||
|
|
||||||
item = self.item
|
|
||||||
try:
|
|
||||||
filepath = item['Path']
|
|
||||||
|
|
||||||
except KeyError:
|
|
||||||
filepath = ""
|
|
||||||
|
|
||||||
else:
|
|
||||||
if "\\\\" in filepath:
|
|
||||||
# append smb protocol
|
|
||||||
filepath = filepath.replace("\\\\", "smb://")
|
|
||||||
filepath = filepath.replace("\\", "/")
|
|
||||||
|
|
||||||
if item.get('VideoType'):
|
|
||||||
videotype = item['VideoType']
|
|
||||||
# Specific format modification
|
|
||||||
if 'Dvd'in videotype:
|
|
||||||
filepath = "%s/VIDEO_TS/VIDEO_TS.IFO" % filepath
|
|
||||||
elif 'Bluray' in videotype:
|
|
||||||
filepath = "%s/BDMV/index.bdmv" % filepath
|
|
||||||
|
|
||||||
if "\\" in filepath:
|
|
||||||
# Local path scenario, with special videotype
|
|
||||||
filepath = filepath.replace("/", "\\")
|
|
||||||
|
|
||||||
return filepath
|
|
|
@ -1,347 +0,0 @@
|
||||||
import xbmc
|
|
||||||
import xbmcgui
|
|
||||||
import xbmcaddon
|
|
||||||
|
|
||||||
import requests
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
|
|
||||||
import Utils as utils
|
|
||||||
from ClientInformation import ClientInformation
|
|
||||||
from requests.packages.urllib3.exceptions import InsecureRequestWarning
|
|
||||||
|
|
||||||
# Disable requests logging
|
|
||||||
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
|
|
||||||
#logging.getLogger("requests").setLevel(logging.WARNING)
|
|
||||||
|
|
||||||
class DownloadUtils():
|
|
||||||
|
|
||||||
# Borg - multiple instances, shared state
|
|
||||||
_shared_state = {}
|
|
||||||
clientInfo = ClientInformation()
|
|
||||||
|
|
||||||
addonName = clientInfo.getAddonName()
|
|
||||||
addon = xbmcaddon.Addon()
|
|
||||||
WINDOW = xbmcgui.Window(10000)
|
|
||||||
|
|
||||||
# Requests session
|
|
||||||
s = None
|
|
||||||
timeout = 60
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
|
|
||||||
self.__dict__ = self._shared_state
|
|
||||||
|
|
||||||
def logMsg(self, msg, lvl=1):
|
|
||||||
|
|
||||||
self.className = self.__class__.__name__
|
|
||||||
utils.logMsg("%s %s" % (self.addonName, self.className), msg, int(lvl))
|
|
||||||
|
|
||||||
def setUsername(self, username):
|
|
||||||
# Reserved for UserClient only
|
|
||||||
self.username = username
|
|
||||||
self.logMsg("Set username: %s" % username, 2)
|
|
||||||
|
|
||||||
def setUserId(self, userId):
|
|
||||||
# Reserved for UserClient only
|
|
||||||
self.userId = userId
|
|
||||||
self.logMsg("Set userId: %s" % userId, 2)
|
|
||||||
|
|
||||||
def setServer(self, server):
|
|
||||||
# Reserved for UserClient only
|
|
||||||
self.server = server
|
|
||||||
self.logMsg("Set server: %s" % server, 2)
|
|
||||||
|
|
||||||
def setToken(self, token):
|
|
||||||
# Reserved for UserClient only
|
|
||||||
self.token = token
|
|
||||||
self.logMsg("Set token: %s" % token, 2)
|
|
||||||
|
|
||||||
def setSSL(self, ssl, sslclient):
|
|
||||||
# Reserved for UserClient only
|
|
||||||
self.sslverify = ssl
|
|
||||||
self.sslclient = sslclient
|
|
||||||
self.logMsg("Verify SSL host certificate: %s" % ssl, 2)
|
|
||||||
self.logMsg("SSL client side certificate: %s" % sslclient, 2)
|
|
||||||
|
|
||||||
def postCapabilities(self, deviceId):
|
|
||||||
|
|
||||||
# Post settings to session
|
|
||||||
url = "{server}/mediabrowser/Sessions/Capabilities/Full"
|
|
||||||
data = {
|
|
||||||
'PlayableMediaTypes': "Audio,Video",
|
|
||||||
'SupportsMediaControl': True,
|
|
||||||
'SupportedCommands': (
|
|
||||||
|
|
||||||
"MoveUp,MoveDown,MoveLeft,MoveRight,Select,"
|
|
||||||
"Back,ToggleContextMenu,ToggleFullscreen,ToggleOsdMenu,"
|
|
||||||
"GoHome,PageUp,NextLetter,GoToSearch,"
|
|
||||||
"GoToSettings,PageDown,PreviousLetter,TakeScreenshot,"
|
|
||||||
"VolumeUp,VolumeDown,ToggleMute,SendString,DisplayMessage,"
|
|
||||||
"SetAudioStreamIndex,SetSubtitleStreamIndex,"
|
|
||||||
|
|
||||||
"Mute,Unmute,SetVolume,"
|
|
||||||
"Play,Playstate,PlayNext"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
self.logMsg("Capabilities URL: %s" % url, 2)
|
|
||||||
self.logMsg("PostData: %s" % data, 2)
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.downloadUrl(url, postBody=data, type="POST")
|
|
||||||
self.logMsg("Posted capabilities to %s" % self.server, 1)
|
|
||||||
except:
|
|
||||||
self.logMsg("Posted capabilities failed.")
|
|
||||||
|
|
||||||
# Attempt at getting sessionId
|
|
||||||
url = "{server}/mediabrowser/Sessions?DeviceId=%s&format=json" % deviceId
|
|
||||||
|
|
||||||
try:
|
|
||||||
result = self.downloadUrl(url)
|
|
||||||
self.logMsg("Session: %s" % result, 2)
|
|
||||||
|
|
||||||
sessionId = result[0][u'Id']
|
|
||||||
self.logMsg("SessionId: %s" % sessionId)
|
|
||||||
self.WINDOW.setProperty("sessionId%s" % self.username, sessionId)
|
|
||||||
except:
|
|
||||||
self.logMsg("Failed to retrieve sessionId.", 1)
|
|
||||||
else:
|
|
||||||
# Post any permanent additional users
|
|
||||||
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()
|
|
||||||
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)
|
|
||||||
|
|
||||||
def startSession(self):
|
|
||||||
|
|
||||||
self.deviceId = self.clientInfo.getMachineId()
|
|
||||||
|
|
||||||
# User is identified from this point
|
|
||||||
# Attach authenticated header to the session
|
|
||||||
verify = None
|
|
||||||
cert = None
|
|
||||||
header = self.getHeader()
|
|
||||||
|
|
||||||
# If user enabled host certificate verification
|
|
||||||
try:
|
|
||||||
verify = self.sslverify
|
|
||||||
cert = self.sslclient
|
|
||||||
except:
|
|
||||||
self.logMsg("Could not load SSL settings.", 1)
|
|
||||||
|
|
||||||
# Start session
|
|
||||||
self.s = requests.Session()
|
|
||||||
self.s.headers = header
|
|
||||||
self.s.verify = verify
|
|
||||||
self.s.cert = cert
|
|
||||||
# Retry connections to the server
|
|
||||||
self.s.mount("http://", requests.adapters.HTTPAdapter(max_retries=1))
|
|
||||||
self.s.mount("https://", requests.adapters.HTTPAdapter(max_retries=1))
|
|
||||||
|
|
||||||
self.logMsg("Requests session started on: %s" % self.server)
|
|
||||||
|
|
||||||
def stopSession(self):
|
|
||||||
try:
|
|
||||||
self.s.close()
|
|
||||||
except:
|
|
||||||
self.logMsg("Requests session could not be terminated.", 1)
|
|
||||||
|
|
||||||
def getHeader(self, authenticate=True):
|
|
||||||
|
|
||||||
clientInfo = self.clientInfo
|
|
||||||
|
|
||||||
deviceName = clientInfo.getDeviceName()
|
|
||||||
deviceId = clientInfo.getMachineId()
|
|
||||||
version = clientInfo.getVersion()
|
|
||||||
|
|
||||||
if not authenticate:
|
|
||||||
# If user is not authenticated
|
|
||||||
auth = 'MediaBrowser Client="Kodi", Device="%s", DeviceId="%s", Version="%s"' % (deviceName, deviceId, version)
|
|
||||||
header = {'Content-type': 'application/json', 'Accept-encoding': 'gzip', 'Accept-Charset': 'UTF-8,*', 'Authorization': auth}
|
|
||||||
|
|
||||||
self.logMsg("Header: %s" % header, 2)
|
|
||||||
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}
|
|
||||||
|
|
||||||
self.logMsg("Header: %s" % header, 2)
|
|
||||||
return header
|
|
||||||
|
|
||||||
def downloadUrl(self, url, postBody=None, type="GET", 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)
|
|
||||||
|
|
||||||
self.logMsg("URL: %s" % url, 2)
|
|
||||||
# Prepare request
|
|
||||||
if type == "GET":
|
|
||||||
r = s.get(url, json=postBody, 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:
|
|
||||||
|
|
||||||
# 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)
|
|
||||||
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
|
|
||||||
|
|
||||||
# Replace for the real values and append api_key
|
|
||||||
url = url.replace("{server}", self.server, 1)
|
|
||||||
url = url.replace("{UserId}", self.userId, 1)
|
|
||||||
|
|
||||||
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)
|
|
||||||
elif type == "POST":
|
|
||||||
r = requests.post(url, json=postBody, headers=header, timeout=timeout, cert=cert, verify=verifyssl)
|
|
||||||
elif type == "DELETE":
|
|
||||||
r = requests.delete(url, json=postBody, headers=header, timeout=timeout, cert=cert, verify=verifyssl)
|
|
||||||
|
|
||||||
# If user is not authenticated
|
|
||||||
elif not authenticate:
|
|
||||||
|
|
||||||
self.logMsg("URL: %s" % url, 2)
|
|
||||||
header = self.getHeader(authenticate=False)
|
|
||||||
verifyssl = False
|
|
||||||
|
|
||||||
# If user enables ssl verification
|
|
||||||
try:
|
|
||||||
verifyssl = self.sslverify
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Prepare request
|
|
||||||
if type == "GET":
|
|
||||||
r = requests.get(url, json=postBody, headers=header, timeout=timeout, verify=verifyssl)
|
|
||||||
elif type == "POST":
|
|
||||||
r = requests.post(url, json=postBody, headers=header, timeout=timeout, verify=verifyssl)
|
|
||||||
|
|
||||||
# Process the response
|
|
||||||
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:
|
|
||||||
self.logMsg("Unable to convert the response for: %s" % url, 1)
|
|
||||||
else:
|
|
||||||
r.raise_for_status()
|
|
||||||
|
|
||||||
return default_link
|
|
||||||
|
|
||||||
# TO REVIEW EXCEPTIONS
|
|
||||||
except requests.exceptions.ConnectionError as e:
|
|
||||||
# Make the addon aware of status
|
|
||||||
if WINDOW.getProperty("Server_online") != "false":
|
|
||||||
self.logMsg("Server unreachable at: %s" % url, 0)
|
|
||||||
self.logMsg(e, 2)
|
|
||||||
WINDOW.setProperty("Server_online", "false")
|
|
||||||
pass
|
|
||||||
|
|
||||||
except requests.exceptions.ConnectTimeout as e:
|
|
||||||
self.logMsg("Server timeout at: %s" % url, 0)
|
|
||||||
self.logMsg(e, 1)
|
|
||||||
|
|
||||||
except requests.exceptions.HTTPError as e:
|
|
||||||
|
|
||||||
if r.status_code == 401:
|
|
||||||
# Unauthorized
|
|
||||||
status = WINDOW.getProperty("Server_status")
|
|
||||||
|
|
||||||
if 'x-application-error-code' in r.headers:
|
|
||||||
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)
|
|
||||||
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
|
|
||||||
pass
|
|
||||||
|
|
||||||
elif (status == "401") or (status == "Auth"):
|
|
||||||
pass
|
|
||||||
|
|
||||||
else:
|
|
||||||
# Tell UserClient token has been revoked.
|
|
||||||
WINDOW.setProperty("Server_status", "401")
|
|
||||||
self.logMsg("HTTP Error: %s" % e, 0)
|
|
||||||
xbmcgui.Dialog().notification("Error connecting", "Unauthorized.", xbmcgui.NOTIFICATION_ERROR)
|
|
||||||
return 401
|
|
||||||
|
|
||||||
elif (r.status_code == 301) or (r.status_code == 302):
|
|
||||||
# Redirects
|
|
||||||
pass
|
|
||||||
elif r.status_code == 400:
|
|
||||||
# Bad requests
|
|
||||||
pass
|
|
||||||
|
|
||||||
except requests.exceptions.SSLError as e:
|
|
||||||
self.logMsg("Invalid SSL certificate for: %s" % url, 0)
|
|
||||||
self.logMsg(e, 1)
|
|
||||||
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
self.logMsg("Unknown error connecting to: %s" % url, 0)
|
|
||||||
self.logMsg(e, 1)
|
|
||||||
|
|
||||||
return default_link
|
|
|
@ -1,694 +0,0 @@
|
||||||
import xbmcaddon
|
|
||||||
import xbmcplugin
|
|
||||||
import xbmc
|
|
||||||
import xbmcgui
|
|
||||||
import xbmcvfs
|
|
||||||
import os, sys
|
|
||||||
import threading
|
|
||||||
import json
|
|
||||||
import urllib
|
|
||||||
import time
|
|
||||||
|
|
||||||
WINDOW = xbmcgui.Window(10000)
|
|
||||||
|
|
||||||
import Utils as utils
|
|
||||||
from ClientInformation import ClientInformation
|
|
||||||
from PlaybackUtils import PlaybackUtils
|
|
||||||
from PlayUtils import PlayUtils
|
|
||||||
from DownloadUtils import DownloadUtils
|
|
||||||
from ReadEmbyDB import ReadEmbyDB
|
|
||||||
from API import API
|
|
||||||
from UserPreferences import UserPreferences
|
|
||||||
|
|
||||||
|
|
||||||
##### Play items via plugin://plugin.video.emby/ #####
|
|
||||||
def doPlayback(id):
|
|
||||||
url = "{server}/mediabrowser/Users/{UserId}/Items/%s?format=json&ImageTypeLimit=1" % id
|
|
||||||
result = DownloadUtils().downloadUrl(url)
|
|
||||||
item = PlaybackUtils().PLAY(result, setup="default")
|
|
||||||
|
|
||||||
#### DO RESET AUTH #####
|
|
||||||
def resetAuth():
|
|
||||||
# User tried login and failed too many times
|
|
||||||
resp = xbmcgui.Dialog().yesno("Warning", "Emby might lock your account if you fail to log in too many times. Proceed anyway?")
|
|
||||||
if resp == 1:
|
|
||||||
xbmc.log("Reset login attempts.")
|
|
||||||
WINDOW.setProperty("Server_status", "Auth")
|
|
||||||
else:
|
|
||||||
xbmc.executebuiltin('Addon.OpenSettings(plugin.video.emby)')
|
|
||||||
|
|
||||||
### ADD ADDITIONAL USERS ###
|
|
||||||
def addUser():
|
|
||||||
|
|
||||||
doUtils = DownloadUtils()
|
|
||||||
clientInfo = ClientInformation()
|
|
||||||
currUser = WINDOW.getProperty("currUser")
|
|
||||||
deviceId = clientInfo.getMachineId()
|
|
||||||
deviceName = clientInfo.getDeviceName()
|
|
||||||
|
|
||||||
# Get session
|
|
||||||
url = "{server}/mediabrowser/Sessions?DeviceId=%s" % deviceId
|
|
||||||
result = doUtils.downloadUrl(url)
|
|
||||||
|
|
||||||
try:
|
|
||||||
sessionId = result[0][u'Id']
|
|
||||||
additionalUsers = result[0][u'AdditionalUsers']
|
|
||||||
# Add user to session
|
|
||||||
userlist = {}
|
|
||||||
users = []
|
|
||||||
url = "{server}/mediabrowser/Users?IsDisabled=false&IsHidden=false"
|
|
||||||
result = doUtils.downloadUrl(url)
|
|
||||||
|
|
||||||
# pull the list of users
|
|
||||||
for user in result:
|
|
||||||
name = user[u'Name']
|
|
||||||
userId = user[u'Id']
|
|
||||||
if currUser not in name:
|
|
||||||
userlist[name] = userId
|
|
||||||
users.append(name)
|
|
||||||
|
|
||||||
# Display dialog if there's additional users
|
|
||||||
if additionalUsers:
|
|
||||||
|
|
||||||
option = xbmcgui.Dialog().select("Add/Remove user from the session", ["Add user", "Remove user"])
|
|
||||||
# Users currently in the session
|
|
||||||
additionalUserlist = {}
|
|
||||||
additionalUsername = []
|
|
||||||
# Users currently in the session
|
|
||||||
for user in additionalUsers:
|
|
||||||
name = user[u'UserName']
|
|
||||||
userId = user[u'UserId']
|
|
||||||
additionalUserlist[name] = userId
|
|
||||||
additionalUsername.append(name)
|
|
||||||
|
|
||||||
if option == 1:
|
|
||||||
# User selected Remove user
|
|
||||||
resp = xbmcgui.Dialog().select("Remove user from the session", additionalUsername)
|
|
||||||
if resp > -1:
|
|
||||||
selected = additionalUsername[resp]
|
|
||||||
selected_userId = additionalUserlist[selected]
|
|
||||||
url = "{server}/mediabrowser/Sessions/%s/Users/%s" % (sessionId, selected_userId)
|
|
||||||
postdata = {}
|
|
||||||
doUtils.downloadUrl(url, postBody=postdata, type="DELETE")
|
|
||||||
xbmcgui.Dialog().notification("Success!", "%s removed from viewing session" % selected, time=1000)
|
|
||||||
|
|
||||||
# clear picture
|
|
||||||
position = WINDOW.getProperty('EmbyAdditionalUserPosition.' + selected_userId)
|
|
||||||
WINDOW.clearProperty('EmbyAdditionalUserImage.' + str(position))
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
return
|
|
||||||
|
|
||||||
elif option == 0:
|
|
||||||
# User selected Add user
|
|
||||||
for adduser in additionalUsername:
|
|
||||||
try: # Remove from selected already added users. It is possible they are hidden.
|
|
||||||
users.remove(adduser)
|
|
||||||
except: pass
|
|
||||||
|
|
||||||
elif option < 0:
|
|
||||||
# User cancelled
|
|
||||||
return
|
|
||||||
|
|
||||||
# Subtract any additional users
|
|
||||||
xbmc.log("Displaying list of users: %s" % users)
|
|
||||||
resp = xbmcgui.Dialog().select("Add user to the session", users)
|
|
||||||
# post additional user
|
|
||||||
if resp > -1:
|
|
||||||
selected = users[resp]
|
|
||||||
selected_userId = userlist[selected]
|
|
||||||
url = "{server}/mediabrowser/Sessions/%s/Users/%s" % (sessionId, selected_userId)
|
|
||||||
postdata = {}
|
|
||||||
doUtils.downloadUrl(url, postBody=postdata, type="POST")
|
|
||||||
xbmcgui.Dialog().notification("Success!", "%s added to viewing session" % selected, time=1000)
|
|
||||||
|
|
||||||
except:
|
|
||||||
xbmc.log("Failed to add user to session.")
|
|
||||||
xbmcgui.Dialog().notification("Error", "Unable to add/remove user from the session.", xbmcgui.NOTIFICATION_ERROR)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Add additional user images
|
|
||||||
#always clear the individual items first
|
|
||||||
totalNodes = 10
|
|
||||||
for i in range(totalNodes):
|
|
||||||
if not WINDOW.getProperty('EmbyAdditionalUserImage.' + str(i)):
|
|
||||||
break
|
|
||||||
WINDOW.clearProperty('EmbyAdditionalUserImage.' + str(i))
|
|
||||||
|
|
||||||
url = "{server}/mediabrowser/Sessions?DeviceId=%s" % deviceId
|
|
||||||
result = doUtils.downloadUrl(url)
|
|
||||||
additionalUsers = result[0][u'AdditionalUsers']
|
|
||||||
count = 0
|
|
||||||
for additionaluser in additionalUsers:
|
|
||||||
url = "{server}/mediabrowser/Users/%s?format=json" % (additionaluser[u'UserId'])
|
|
||||||
result = doUtils.downloadUrl(url)
|
|
||||||
WINDOW.setProperty("EmbyAdditionalUserImage." + str(count),API().getUserArtwork(result,"Primary"))
|
|
||||||
WINDOW.setProperty("EmbyAdditionalUserPosition." + str(additionaluser[u'UserId']),str(count))
|
|
||||||
count +=1
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# THEME MUSIC/VIDEOS
|
|
||||||
def getThemeMedia():
|
|
||||||
|
|
||||||
doUtils = DownloadUtils()
|
|
||||||
playUtils = PlayUtils()
|
|
||||||
|
|
||||||
currUser = WINDOW.getProperty('currUser')
|
|
||||||
server = WINDOW.getProperty('server%s' % currUser)
|
|
||||||
playback = None
|
|
||||||
|
|
||||||
library = xbmc.translatePath("special://profile/addon_data/plugin.video.emby/library/").decode('utf-8')
|
|
||||||
|
|
||||||
# Choose playback method
|
|
||||||
resp = xbmcgui.Dialog().select("Choose playback method for your themes", ["Direct Play", "Direct Stream"])
|
|
||||||
if resp == 0:
|
|
||||||
# Direct Play
|
|
||||||
playback = "DirectPlay"
|
|
||||||
elif resp == 1:
|
|
||||||
# Direct Stream
|
|
||||||
playback = "DirectStream"
|
|
||||||
else:return
|
|
||||||
|
|
||||||
# Set custom path for user
|
|
||||||
tvtunes_path = xbmc.translatePath("special://profile/addon_data/script.tvtunes/").decode('utf-8')
|
|
||||||
if xbmcvfs.exists(tvtunes_path):
|
|
||||||
tvtunes = xbmcaddon.Addon(id="script.tvtunes")
|
|
||||||
tvtunes.setSetting('custom_path_enable', "true")
|
|
||||||
tvtunes.setSetting('custom_path', library)
|
|
||||||
xbmc.log("TV Tunes custom path is enabled and set.")
|
|
||||||
else:
|
|
||||||
# if it does not exist this will not work so warn user, often they need to edit the settings first for it to be created.
|
|
||||||
dialog = xbmcgui.Dialog()
|
|
||||||
dialog.ok('Warning', 'The settings file does not exist in tvtunes. Go to the tvtunes addon and change a setting, then come back and re-run')
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
# Create library directory
|
|
||||||
if not xbmcvfs.exists(library):
|
|
||||||
xbmcvfs.mkdir(library)
|
|
||||||
|
|
||||||
# Get every user view Id
|
|
||||||
userViews = []
|
|
||||||
url = "{server}/mediabrowser/Users/{UserId}/Items?format=json"
|
|
||||||
result = doUtils.downloadUrl(url)
|
|
||||||
|
|
||||||
for view in result[u'Items']:
|
|
||||||
userviewId = view[u'Id']
|
|
||||||
userViews.append(userviewId)
|
|
||||||
|
|
||||||
|
|
||||||
# Get Ids with Theme Videos
|
|
||||||
itemIds = {}
|
|
||||||
for view in userViews:
|
|
||||||
url = "{server}/mediabrowser/Users/{UserId}/Items?HasThemeVideo=True&ParentId=%s&format=json" % view
|
|
||||||
result = doUtils.downloadUrl(url)
|
|
||||||
if result[u'TotalRecordCount'] != 0:
|
|
||||||
for item in result[u'Items']:
|
|
||||||
itemId = item[u'Id']
|
|
||||||
folderName = item[u'Name']
|
|
||||||
folderName = utils.normalize_string(folderName.encode('utf-8'))
|
|
||||||
itemIds[itemId] = folderName
|
|
||||||
|
|
||||||
# Get paths for theme videos
|
|
||||||
for itemId in itemIds:
|
|
||||||
nfo_path = xbmc.translatePath("special://profile/addon_data/plugin.video.emby/library/%s/" % itemIds[itemId])
|
|
||||||
# Create folders for each content
|
|
||||||
if not xbmcvfs.exists(nfo_path):
|
|
||||||
xbmcvfs.mkdir(nfo_path)
|
|
||||||
# Where to put the nfos
|
|
||||||
nfo_path = "%s%s" % (nfo_path, "tvtunes.nfo")
|
|
||||||
|
|
||||||
url = "{server}/mediabrowser/Items/%s/ThemeVideos?format=json" % itemId
|
|
||||||
result = doUtils.downloadUrl(url)
|
|
||||||
|
|
||||||
# Create nfo and write themes to it
|
|
||||||
nfo_file = open(nfo_path, 'w')
|
|
||||||
pathstowrite = ""
|
|
||||||
# May be more than one theme
|
|
||||||
for theme in result[u'Items']:
|
|
||||||
if playback == "DirectPlay":
|
|
||||||
playurl = playUtils.directPlay(theme)
|
|
||||||
else:
|
|
||||||
playurl = playUtils.directStream(result, server, theme[u'Id'], "ThemeVideo")
|
|
||||||
pathstowrite += ('<file>%s</file>' % playurl.encode('utf-8'))
|
|
||||||
|
|
||||||
# Check if the item has theme songs and add them
|
|
||||||
url = "{server}/mediabrowser/Items/%s/ThemeSongs?format=json" % itemId
|
|
||||||
result = doUtils.downloadUrl(url)
|
|
||||||
|
|
||||||
# May be more than one theme
|
|
||||||
for theme in result[u'Items']:
|
|
||||||
if playback == "DirectPlay":
|
|
||||||
playurl = playUtils.directPlay(theme)
|
|
||||||
else:
|
|
||||||
playurl = playUtils.directStream(result, server, theme[u'Id'], "Audio")
|
|
||||||
pathstowrite += ('<file>%s</file>' % playurl.encode('utf-8'))
|
|
||||||
|
|
||||||
nfo_file.write(
|
|
||||||
'<tvtunes>%s</tvtunes>' % pathstowrite
|
|
||||||
)
|
|
||||||
# Close nfo file
|
|
||||||
nfo_file.close()
|
|
||||||
|
|
||||||
# Get Ids with Theme songs
|
|
||||||
musicitemIds = {}
|
|
||||||
for view in userViews:
|
|
||||||
url = "{server}/mediabrowser/Users/{UserId}/Items?HasThemeSong=True&ParentId=%s&format=json" % view
|
|
||||||
result = doUtils.downloadUrl(url)
|
|
||||||
if result[u'TotalRecordCount'] != 0:
|
|
||||||
for item in result[u'Items']:
|
|
||||||
itemId = item[u'Id']
|
|
||||||
folderName = item[u'Name']
|
|
||||||
folderName = utils.normalize_string(folderName.encode('utf-8'))
|
|
||||||
musicitemIds[itemId] = folderName
|
|
||||||
|
|
||||||
# Get paths
|
|
||||||
for itemId in musicitemIds:
|
|
||||||
|
|
||||||
# if the item was already processed with video themes back out
|
|
||||||
if itemId in itemIds:
|
|
||||||
continue
|
|
||||||
|
|
||||||
nfo_path = xbmc.translatePath("special://profile/addon_data/plugin.video.emby/library/%s/" % musicitemIds[itemId])
|
|
||||||
# Create folders for each content
|
|
||||||
if not xbmcvfs.exists(nfo_path):
|
|
||||||
xbmcvfs.mkdir(nfo_path)
|
|
||||||
# Where to put the nfos
|
|
||||||
nfo_path = "%s%s" % (nfo_path, "tvtunes.nfo")
|
|
||||||
|
|
||||||
url = "{server}/mediabrowser/Items/%s/ThemeSongs?format=json" % itemId
|
|
||||||
result = doUtils.downloadUrl(url)
|
|
||||||
|
|
||||||
# Create nfo and write themes to it
|
|
||||||
nfo_file = open(nfo_path, 'w')
|
|
||||||
pathstowrite = ""
|
|
||||||
# May be more than one theme
|
|
||||||
for theme in result[u'Items']:
|
|
||||||
if playback == "DirectPlay":
|
|
||||||
playurl = playUtils.directPlay(theme)
|
|
||||||
else:
|
|
||||||
playurl = playUtils.directStream(result, server, theme[u'Id'], "Audio")
|
|
||||||
pathstowrite += ('<file>%s</file>' % playurl.encode('utf-8'))
|
|
||||||
|
|
||||||
nfo_file.write(
|
|
||||||
'<tvtunes>%s</tvtunes>' % pathstowrite
|
|
||||||
)
|
|
||||||
# Close nfo file
|
|
||||||
nfo_file.close()
|
|
||||||
|
|
||||||
def userPreferences():
|
|
||||||
doUtils = DownloadUtils()
|
|
||||||
addonSettings = xbmcaddon.Addon(id='plugin.video.emby')
|
|
||||||
userPreferencesPage = UserPreferences("script-emby-kodi-UserPreferences.xml", addonSettings.getAddonInfo('path'), "default", "1080i")
|
|
||||||
url = "{server}/mediabrowser/Users/{UserId}"
|
|
||||||
result = doUtils.downloadUrl(url)
|
|
||||||
configuration = result[u'Configuration']
|
|
||||||
userPreferencesPage.setConfiguration(configuration)
|
|
||||||
userPreferencesPage.setName(result[u'Name'])
|
|
||||||
userPreferencesPage.setImage(API().getUserArtwork(result,"Primary"))
|
|
||||||
|
|
||||||
userPreferencesPage.doModal()
|
|
||||||
if userPreferencesPage.isSave():
|
|
||||||
url = "{server}/mediabrowser/Users/{UserId}/Configuration"
|
|
||||||
postdata = userPreferencesPage.getConfiguration()
|
|
||||||
doUtils.downloadUrl(url, postBody=postdata, type="POST")
|
|
||||||
|
|
||||||
##### BROWSE EMBY CHANNELS #####
|
|
||||||
def BrowseChannels(id, folderid=None):
|
|
||||||
|
|
||||||
_addon_id = int(sys.argv[1])
|
|
||||||
_addon_url = sys.argv[0]
|
|
||||||
|
|
||||||
xbmcplugin.setContent(int(sys.argv[1]), 'files')
|
|
||||||
if folderid:
|
|
||||||
url = "{server}/mediabrowser/Channels/" + id + "/Items?userid={UserId}&folderid=" + folderid + "&format=json"
|
|
||||||
else:
|
|
||||||
if id == "0": # id 0 is the root channels folder
|
|
||||||
url = "{server}/mediabrowser/Channels?{UserId}&format=json"
|
|
||||||
else:
|
|
||||||
url = "{server}/mediabrowser/Channels/" + id + "/Items?userid={UserId}&format=json"
|
|
||||||
|
|
||||||
results = DownloadUtils().downloadUrl(url)
|
|
||||||
if results:
|
|
||||||
result = results.get("Items")
|
|
||||||
if(result == None):
|
|
||||||
result = []
|
|
||||||
|
|
||||||
item_count = len(result)
|
|
||||||
current_item = 1;
|
|
||||||
|
|
||||||
for item in result:
|
|
||||||
id=str(item.get("Id")).encode('utf-8')
|
|
||||||
type=item.get("Type").encode('utf-8')
|
|
||||||
|
|
||||||
|
|
||||||
if(item.get("Name") != None):
|
|
||||||
tempTitle = item.get("Name")
|
|
||||||
tempTitle=tempTitle.encode('utf-8')
|
|
||||||
else:
|
|
||||||
tempTitle = "Missing Title"
|
|
||||||
|
|
||||||
if type=="ChannelFolderItem":
|
|
||||||
isFolder = True
|
|
||||||
else:
|
|
||||||
isFolder = False
|
|
||||||
item_type = str(type).encode('utf-8')
|
|
||||||
|
|
||||||
if(item.get("ChannelId") != None):
|
|
||||||
channelId = str(item.get("ChannelId")).encode('utf-8')
|
|
||||||
|
|
||||||
channelName = ''
|
|
||||||
if(item.get("ChannelName") != None):
|
|
||||||
channelName = item.get("ChannelName").encode('utf-8')
|
|
||||||
|
|
||||||
if(item.get("PremiereDate") != None):
|
|
||||||
premieredatelist = (item.get("PremiereDate")).split("T")
|
|
||||||
premieredate = premieredatelist[0]
|
|
||||||
else:
|
|
||||||
premieredate = ""
|
|
||||||
|
|
||||||
#mediaStreams=API().getMediaStreams(item, True)
|
|
||||||
|
|
||||||
#people = API().getPeople(item)
|
|
||||||
|
|
||||||
# Process Genres
|
|
||||||
genre = API().getGenre(item)
|
|
||||||
|
|
||||||
# Process UserData
|
|
||||||
userData = item.get("UserData")
|
|
||||||
PlaybackPositionTicks = '100'
|
|
||||||
overlay = "0"
|
|
||||||
favorite = "False"
|
|
||||||
seekTime = 0
|
|
||||||
if(userData != None):
|
|
||||||
if userData.get("Played") != True:
|
|
||||||
overlay = "7"
|
|
||||||
watched = "true"
|
|
||||||
else:
|
|
||||||
overlay = "6"
|
|
||||||
watched = "false"
|
|
||||||
if userData.get("IsFavorite") == True:
|
|
||||||
overlay = "5"
|
|
||||||
favorite = "True"
|
|
||||||
else:
|
|
||||||
favorite = "False"
|
|
||||||
if userData.get("PlaybackPositionTicks") != None:
|
|
||||||
PlaybackPositionTicks = str(userData.get("PlaybackPositionTicks"))
|
|
||||||
reasonableTicks = int(userData.get("PlaybackPositionTicks")) / 1000
|
|
||||||
seekTime = reasonableTicks / 10000
|
|
||||||
|
|
||||||
playCount = 0
|
|
||||||
if(userData != None and userData.get("Played") == True):
|
|
||||||
playCount = 1
|
|
||||||
# Populate the details list
|
|
||||||
details={'title' : tempTitle,
|
|
||||||
'channelname' : channelName,
|
|
||||||
'plot' : item.get("Overview"),
|
|
||||||
'Overlay' : overlay,
|
|
||||||
'playcount' : str(playCount)}
|
|
||||||
|
|
||||||
if item.get("Type") == "ChannelVideoItem":
|
|
||||||
xbmcplugin.setContent(_addon_id, 'movies')
|
|
||||||
elif item.get("Type") == "ChannelAudioItem":
|
|
||||||
xbmcplugin.setContent(_addon_id, 'songs')
|
|
||||||
|
|
||||||
# Populate the extraData list
|
|
||||||
extraData={'thumb' : API().getArtwork(item, "Primary") ,
|
|
||||||
'fanart_image' : API().getArtwork(item, "Backdrop") ,
|
|
||||||
'poster' : API().getArtwork(item, "poster") ,
|
|
||||||
'tvshow.poster': API().getArtwork(item, "tvshow.poster") ,
|
|
||||||
'banner' : API().getArtwork(item, "Banner") ,
|
|
||||||
'clearlogo' : API().getArtwork(item, "Logo") ,
|
|
||||||
'discart' : API().getArtwork(item, "Disc") ,
|
|
||||||
'clearart' : API().getArtwork(item, "Art") ,
|
|
||||||
'landscape' : API().getArtwork(item, "Thumb") ,
|
|
||||||
'id' : id ,
|
|
||||||
'rating' : item.get("CommunityRating"),
|
|
||||||
'year' : item.get("ProductionYear"),
|
|
||||||
'premieredate' : premieredate,
|
|
||||||
'genre' : genre,
|
|
||||||
'playcount' : str(playCount),
|
|
||||||
'itemtype' : item_type}
|
|
||||||
|
|
||||||
if extraData['thumb'] == '':
|
|
||||||
extraData['thumb'] = extraData['fanart_image']
|
|
||||||
|
|
||||||
liz = xbmcgui.ListItem(tempTitle)
|
|
||||||
|
|
||||||
artTypes=['poster', 'tvshow.poster', 'fanart_image', 'clearlogo', 'discart', 'banner', 'clearart', 'landscape', 'small_poster', 'tiny_poster', 'medium_poster','small_fanartimage', 'medium_fanartimage', 'medium_landscape', 'fanart_noindicators']
|
|
||||||
|
|
||||||
for artType in artTypes:
|
|
||||||
imagePath=str(extraData.get(artType,''))
|
|
||||||
liz=PlaybackUtils().setArt(liz,artType, imagePath)
|
|
||||||
|
|
||||||
liz.setThumbnailImage(API().getArtwork(item, "Primary"))
|
|
||||||
liz.setIconImage('DefaultTVShows.png')
|
|
||||||
#liz.setInfo( type="Video", infoLabels={ "Rating": item.get("CommunityRating") })
|
|
||||||
#liz.setInfo( type="Video", infoLabels={ "Plot": item.get("Overview") })
|
|
||||||
|
|
||||||
if type=="Channel":
|
|
||||||
file = _addon_url + "?id=%s&mode=channels"%id
|
|
||||||
xbmcplugin.addDirectoryItem(handle=_addon_id, url=file, listitem=liz, isFolder=True)
|
|
||||||
|
|
||||||
elif isFolder == True:
|
|
||||||
file = _addon_url + "?id=%s&mode=channelsfolder&folderid=%s" %(channelId, id)
|
|
||||||
xbmcplugin.addDirectoryItem(handle=_addon_id, url=file, listitem=liz, isFolder=True)
|
|
||||||
else:
|
|
||||||
file = _addon_url + "?id=%s&mode=play"%id
|
|
||||||
liz.setProperty('IsPlayable', 'true')
|
|
||||||
xbmcplugin.addDirectoryItem(handle=_addon_id, url=file, listitem=liz)
|
|
||||||
|
|
||||||
xbmcplugin.endOfDirectory(handle=int(sys.argv[1]))
|
|
||||||
|
|
||||||
##### GET NEXTUP EPISODES FOR TAGNAME #####
|
|
||||||
def getNextUpEpisodes(tagname,limit):
|
|
||||||
count=0
|
|
||||||
|
|
||||||
#if the addon is called with nextup parameter, we return the nextepisodes list of the given tagname
|
|
||||||
xbmcplugin.setContent(int(sys.argv[1]), 'episodes')
|
|
||||||
# First we get a list of all the in-progress TV shows - filtered by tag
|
|
||||||
json_query_string = xbmc.executeJSONRPC('{"jsonrpc": "2.0", "method": "VideoLibrary.GetTVShows", "params": { "sort": { "order": "descending", "method": "lastplayed" }, "filter": {"and": [{"operator":"true", "field":"inprogress", "value":""}, {"operator": "is", "field": "tag", "value": "%s"}]}, "properties": [ "title", "studio", "mpaa", "file", "art" ] }, "id": "libTvShows"}' %tagname)
|
|
||||||
|
|
||||||
json_result = json.loads(json_query_string)
|
|
||||||
# If we found any, find the oldest unwatched show for each one.
|
|
||||||
if json_result.has_key('result') and json_result['result'].has_key('tvshows'):
|
|
||||||
for item in json_result['result']['tvshows']:
|
|
||||||
|
|
||||||
# If Ignore Specials is true only choose episodes from seasons greater than 0.
|
|
||||||
if utils.settings("ignoreSpecialsNextEpisodes")=="true":
|
|
||||||
json_query2 = xbmc.executeJSONRPC('{"jsonrpc": "2.0", "method": "VideoLibrary.GetEpisodes", "params": { "tvshowid": %d, "sort": {"method":"episode"}, "filter": {"and": [ {"field": "playcount", "operator": "lessthan", "value":"1"}, {"field": "season", "operator": "greaterthan", "value": "0"} ]}, "properties": [ "title", "playcount", "season", "episode", "showtitle", "plot", "file", "rating", "resume", "tvshowid", "art", "streamdetails", "firstaired", "runtime", "writer", "dateadded", "lastplayed" ], "limits":{"end":1}}, "id": "1"}' %item['tvshowid'])
|
|
||||||
else:
|
|
||||||
json_query2 = xbmc.executeJSONRPC('{"jsonrpc": "2.0", "method": "VideoLibrary.GetEpisodes", "params": { "tvshowid": %d, "sort": {"method":"episode"}, "filter": {"field": "playcount", "operator": "lessthan", "value":"1"}, "properties": [ "title", "playcount", "season", "episode", "showtitle", "plot", "file", "rating", "resume", "tvshowid", "art", "streamdetails", "firstaired", "runtime", "writer", "dateadded", "lastplayed" ], "limits":{"end":1}}, "id": "1"}' %item['tvshowid'])
|
|
||||||
|
|
||||||
if json_query2:
|
|
||||||
json_query2 = json.loads(json_query2)
|
|
||||||
if json_query2.has_key('result') and json_query2['result'].has_key('episodes'):
|
|
||||||
for item in json_query2['result']['episodes']:
|
|
||||||
liz = createListItem(item)
|
|
||||||
xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=item['file'], listitem=liz)
|
|
||||||
count +=1
|
|
||||||
if count == limit:
|
|
||||||
break
|
|
||||||
xbmcplugin.endOfDirectory(handle=int(sys.argv[1]))
|
|
||||||
|
|
||||||
def getInProgressEpisodes(tagname,limit):
|
|
||||||
count = 0
|
|
||||||
#if the addon is called with inprogressepisodes parameter, we return the inprogressepisodes list of the given tagname
|
|
||||||
xbmcplugin.setContent(int(sys.argv[1]), 'episodes')
|
|
||||||
# First we get a list of all the in-progress TV shows - filtered by tag
|
|
||||||
json_query_string = xbmc.executeJSONRPC('{"jsonrpc": "2.0", "method": "VideoLibrary.GetTVShows", "params": { "sort": { "order": "descending", "method": "lastplayed" }, "filter": {"and": [{"operator":"true", "field":"inprogress", "value":""}, {"operator": "contains", "field": "tag", "value": "%s"}]}, "properties": [ "title", "studio", "mpaa", "file", "art" ] }, "id": "libTvShows"}' %tagname)
|
|
||||||
json_result = json.loads(json_query_string)
|
|
||||||
# If we found any, find all in progress episodes for each one.
|
|
||||||
if json_result.has_key('result') and json_result['result'].has_key('tvshows'):
|
|
||||||
for item in json_result['result']['tvshows']:
|
|
||||||
json_query2 = xbmc.executeJSONRPC('{"jsonrpc": "2.0", "method": "VideoLibrary.GetEpisodes", "params": { "tvshowid": %d, "sort": {"method":"episode"}, "filter": {"field": "inprogress", "operator": "true", "value":""}, "properties": [ "title", "playcount", "season", "episode", "showtitle", "plot", "file", "rating", "resume", "tvshowid", "art", "cast", "streamdetails", "firstaired", "runtime", "writer", "dateadded", "lastplayed" ]}, "id": "1"}' %item['tvshowid'])
|
|
||||||
|
|
||||||
if json_query2:
|
|
||||||
json_query2 = json.loads(json_query2)
|
|
||||||
if json_query2.has_key('result') and json_query2['result'].has_key('episodes'):
|
|
||||||
for item in json_query2['result']['episodes']:
|
|
||||||
liz = createListItem(item)
|
|
||||||
xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=item['file'], listitem=liz)
|
|
||||||
count +=1
|
|
||||||
if count == limit:
|
|
||||||
break
|
|
||||||
xbmcplugin.endOfDirectory(handle=int(sys.argv[1]))
|
|
||||||
|
|
||||||
def getRecentEpisodes(tagname,limit):
|
|
||||||
#if the addon is called with recentepisodes parameter, we return the recentepisodes list of the given tagname
|
|
||||||
xbmcplugin.setContent(int(sys.argv[1]), 'episodes')
|
|
||||||
# First we get a list of all the TV shows - filtered by tag
|
|
||||||
json_query_string = xbmc.executeJSONRPC('{"jsonrpc": "2.0", "method": "VideoLibrary.GetTVShows", "params": { "sort": { "order": "descending", "method": "dateadded" }, "properties": [ "title","sorttitle" ], "filter": {"operator": "contains", "field": "tag", "value": "%s"} }, "id": "libTvShows"}' %tagname)
|
|
||||||
json_result = json.loads(json_query_string)
|
|
||||||
|
|
||||||
# If we found any, put all tv show id's in a list
|
|
||||||
if json_result.has_key('result') and json_result['result'].has_key('tvshows'):
|
|
||||||
alltvshowIds = list()
|
|
||||||
for tvshow in json_result['result']['tvshows']:
|
|
||||||
alltvshowIds.append(tvshow["tvshowid"])
|
|
||||||
alltvshowIds = set(alltvshowIds)
|
|
||||||
|
|
||||||
#get all recently added episodes
|
|
||||||
json_query2 = xbmc.executeJSONRPC('{"jsonrpc": "2.0", "method": "VideoLibrary.GetEpisodes", "params": { "sort": {"order": "descending", "method": "dateadded"}, "filter": {"field": "playcount", "operator": "lessthan", "value":"1"}, "properties": [ "title", "playcount", "season", "episode", "showtitle", "plot", "file", "rating", "resume", "tvshowid", "art", "streamdetails", "firstaired", "runtime", "cast", "writer", "dateadded", "lastplayed" ]}, "limits":{"end":%d}, "id": "1"}' %limit)
|
|
||||||
count = 0
|
|
||||||
if json_query2:
|
|
||||||
json_query2 = json.loads(json_query2)
|
|
||||||
if json_query2.has_key('result') and json_query2['result'].has_key('episodes'):
|
|
||||||
for item in json_query2['result']['episodes']:
|
|
||||||
if item["tvshowid"] in alltvshowIds:
|
|
||||||
liz = createListItem(item)
|
|
||||||
xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=item['file'], listitem=liz)
|
|
||||||
count += 1
|
|
||||||
if count == limit:
|
|
||||||
break
|
|
||||||
xbmcplugin.endOfDirectory(handle=int(sys.argv[1]))
|
|
||||||
|
|
||||||
def createListItem(item):
|
|
||||||
|
|
||||||
liz = xbmcgui.ListItem(item['title'])
|
|
||||||
liz.setInfo( type="Video", infoLabels={ "Title": item['title'] })
|
|
||||||
liz.setProperty('IsPlayable', 'true')
|
|
||||||
liz.setInfo( type="Video", infoLabels={ "duration": str(item['runtime']/60) })
|
|
||||||
|
|
||||||
if "episode" in item:
|
|
||||||
episode = "%.2d" % float(item['episode'])
|
|
||||||
liz.setInfo( type="Video", infoLabels={ "Episode": item['episode'] })
|
|
||||||
|
|
||||||
if "season" in item:
|
|
||||||
season = "%.2d" % float(item['season'])
|
|
||||||
liz.setInfo( type="Video", infoLabels={ "Season": item['season'] })
|
|
||||||
|
|
||||||
if season and episode:
|
|
||||||
episodeno = "s%se%s" %(season,episode)
|
|
||||||
liz.setProperty("episodeno", episodeno)
|
|
||||||
|
|
||||||
if "firstaired" in item:
|
|
||||||
liz.setInfo( type="Video", infoLabels={ "Premiered": item['firstaired'] })
|
|
||||||
|
|
||||||
plot = item['plot']
|
|
||||||
liz.setInfo( type="Video", infoLabels={ "Plot": plot })
|
|
||||||
|
|
||||||
if "showtitle" in item:
|
|
||||||
liz.setInfo( type="Video", infoLabels={ "TVshowTitle": item['showtitle'] })
|
|
||||||
|
|
||||||
if "rating" in item:
|
|
||||||
liz.setInfo( type="Video", infoLabels={ "Rating": str(round(float(item['rating']),1)) })
|
|
||||||
liz.setInfo( type="Video", infoLabels={ "Playcount": item['playcount'] })
|
|
||||||
if "director" in item:
|
|
||||||
liz.setInfo( type="Video", infoLabels={ "Director": " / ".join(item['director']) })
|
|
||||||
if "writer" in item:
|
|
||||||
liz.setInfo( type="Video", infoLabels={ "Writer": " / ".join(item['writer']) })
|
|
||||||
|
|
||||||
if "cast" in item:
|
|
||||||
listCast = []
|
|
||||||
listCastAndRole = []
|
|
||||||
for castmember in item["cast"]:
|
|
||||||
listCast.append( castmember["name"] )
|
|
||||||
listCastAndRole.append( (castmember["name"], castmember["role"]) )
|
|
||||||
cast = [listCast, listCastAndRole]
|
|
||||||
liz.setInfo( type="Video", infoLabels={ "Cast": cast[0] })
|
|
||||||
liz.setInfo( type="Video", infoLabels={ "CastAndRole": cast[1] })
|
|
||||||
|
|
||||||
liz.setProperty("resumetime", str(item['resume']['position']))
|
|
||||||
liz.setProperty("totaltime", str(item['resume']['total']))
|
|
||||||
liz.setArt(item['art'])
|
|
||||||
liz.setThumbnailImage(item['art'].get('thumb',''))
|
|
||||||
liz.setIconImage('DefaultTVShows.png')
|
|
||||||
liz.setProperty("dbid", str(item['episodeid']))
|
|
||||||
liz.setProperty("fanart_image", item['art'].get('tvshow.fanart',''))
|
|
||||||
for key, value in item['streamdetails'].iteritems():
|
|
||||||
for stream in value:
|
|
||||||
liz.addStreamInfo( key, stream )
|
|
||||||
|
|
||||||
return liz
|
|
||||||
|
|
||||||
##### GET EXTRAFANART FOR LISTITEM #####
|
|
||||||
def getExtraFanArt():
|
|
||||||
itemPath = ""
|
|
||||||
embyId = ""
|
|
||||||
|
|
||||||
#get extrafanart for listitem - this will only be used for skins that actually call the listitem's path + fanart dir...
|
|
||||||
try:
|
|
||||||
#only do this if the listitem has actually changed
|
|
||||||
itemPath = xbmc.getInfoLabel("ListItem.FileNameAndPath")
|
|
||||||
|
|
||||||
if not itemPath:
|
|
||||||
itemPath = xbmc.getInfoLabel("ListItem.Path")
|
|
||||||
|
|
||||||
if ("/tvshows/" in itemPath or "/musicvideos/" in itemPath or "/movies/" in itemPath):
|
|
||||||
embyId = itemPath.split("/")[-2]
|
|
||||||
|
|
||||||
utils.logMsg("%s %s" % ("Emby addon", "getExtraFanArt"), "requesting extraFanArt for Id: " + embyId, 1)
|
|
||||||
|
|
||||||
#we need to store the images locally for this to work because of the caching system in xbmc
|
|
||||||
fanartDir = xbmc.translatePath("special://thumbnails/emby/" + embyId + "/")
|
|
||||||
|
|
||||||
if not xbmcvfs.exists(fanartDir):
|
|
||||||
#download the images to the cache directory
|
|
||||||
xbmcvfs.mkdir(fanartDir)
|
|
||||||
item = ReadEmbyDB().getFullItem(embyId)
|
|
||||||
if item != None:
|
|
||||||
if item.has_key("BackdropImageTags"):
|
|
||||||
if(len(item["BackdropImageTags"]) > 0):
|
|
||||||
WINDOW = xbmcgui.Window(10000)
|
|
||||||
username = WINDOW.getProperty('currUser')
|
|
||||||
server = WINDOW.getProperty('server%s' % username)
|
|
||||||
totalbackdrops = len(item["BackdropImageTags"])
|
|
||||||
count = 0
|
|
||||||
for backdrop in item["BackdropImageTags"]:
|
|
||||||
backgroundUrl = "%s/mediabrowser/Items/%s/Images/Backdrop/%s/?MaxWidth=10000&MaxHeight=10000&Format=original&Tag=%s&EnableImageEnhancers=false" % (server, embyId, str(count), backdrop)
|
|
||||||
count += 1
|
|
||||||
fanartFile = os.path.join(fanartDir,"fanart" + backdrop + ".jpg")
|
|
||||||
li = xbmcgui.ListItem(backdrop, path=fanartFile)
|
|
||||||
xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=fanartFile, listitem=li)
|
|
||||||
xbmcvfs.copy(backgroundUrl,fanartFile)
|
|
||||||
|
|
||||||
else:
|
|
||||||
#use existing cached images
|
|
||||||
dirs, files = xbmcvfs.listdir(fanartDir)
|
|
||||||
count = 1
|
|
||||||
for file in files:
|
|
||||||
count +=1
|
|
||||||
li = xbmcgui.ListItem(file, path=os.path.join(fanartDir,file))
|
|
||||||
xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=os.path.join(fanartDir,file), listitem=li)
|
|
||||||
except Exception as e:
|
|
||||||
utils.logMsg("%s %s" % ("Emby addon", "Error in getExtraFanArt"), str(e), 1)
|
|
||||||
pass
|
|
||||||
|
|
||||||
#always do endofdirectory to prevent errors in the logs
|
|
||||||
xbmcplugin.endOfDirectory(int(sys.argv[1]))
|
|
||||||
|
|
||||||
def addDirectoryItem(label, path, folder=True):
|
|
||||||
li = xbmcgui.ListItem(label, path=path)
|
|
||||||
li.setThumbnailImage("special://home/addons/plugin.video.emby/icon.png")
|
|
||||||
li.setArt({"fanart":"special://home/addons/plugin.video.emby/fanart.jpg"})
|
|
||||||
li.setArt({"landscape":"special://home/addons/plugin.video.emby/fanart.jpg"})
|
|
||||||
xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=path, listitem=li, isFolder=folder)
|
|
||||||
|
|
||||||
# if the addon is called without parameters we show the listing...
|
|
||||||
def doMainListing():
|
|
||||||
|
|
||||||
xbmcplugin.setContent(int(sys.argv[1]), 'files')
|
|
||||||
#get emby nodes from the window props
|
|
||||||
embyProperty = WINDOW.getProperty("Emby.nodes.total")
|
|
||||||
if embyProperty:
|
|
||||||
totalNodes = int(embyProperty)
|
|
||||||
for i in range(totalNodes):
|
|
||||||
path = WINDOW.getProperty("Emby.nodes.%s.index" %str(i))
|
|
||||||
if not path:
|
|
||||||
path = WINDOW.getProperty("Emby.nodes.%s.content" %str(i))
|
|
||||||
label = WINDOW.getProperty("Emby.nodes.%s.title" %str(i))
|
|
||||||
if path:
|
|
||||||
addDirectoryItem(label, path)
|
|
||||||
|
|
||||||
# some extra entries for settings and stuff. TODO --> localize the labels
|
|
||||||
addDirectoryItem("Settings", "plugin://plugin.video.emby/?mode=settings")
|
|
||||||
addDirectoryItem("Perform manual sync", "plugin://plugin.video.emby/?mode=manualsync")
|
|
||||||
addDirectoryItem("Add user to session", "plugin://plugin.video.emby/?mode=adduser")
|
|
||||||
addDirectoryItem("Configure user preferences", "plugin://plugin.video.emby/?mode=userprefs")
|
|
||||||
addDirectoryItem("Perform local database reset (full resync)", "plugin://plugin.video.emby/?mode=reset")
|
|
||||||
addDirectoryItem("Cache all images to Kodi texture cache (advanced)", "plugin://plugin.video.emby/?mode=texturecache")
|
|
||||||
addDirectoryItem("Sync Emby Theme Media to Kodi", "plugin://plugin.video.emby/?mode=thememedia")
|
|
||||||
|
|
||||||
xbmcplugin.endOfDirectory(int(sys.argv[1]))
|
|
|
@ -1,150 +0,0 @@
|
||||||
#################################################################################################
|
|
||||||
# Kodi Monitor
|
|
||||||
# Watched events that occur in Kodi, like setting media watched
|
|
||||||
#################################################################################################
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
class Kodi_Monitor( xbmc.Monitor ):
|
|
||||||
|
|
||||||
WINDOW = xbmcgui.Window(10000)
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
xbmc.Monitor.__init__(self)
|
|
||||||
|
|
||||||
def logMsg(self, msg, lvl = 1):
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
def onScanFinished(self, library):
|
|
||||||
utils.window('kodiScan', clear=True)
|
|
||||||
self.logMsg("Kodi library scan finished.", 2)
|
|
||||||
|
|
||||||
#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):
|
|
||||||
|
|
||||||
WINDOW = self.WINDOW
|
|
||||||
downloadUtils = DownloadUtils()
|
|
||||||
#player started playing an item -
|
|
||||||
if ("Playlist.OnAdd" in method or "Player.OnPlay" in method):
|
|
||||||
|
|
||||||
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 (utils.settings('useDirectPaths')=='true' and not type == "song") or (type == "song" and utils.settings('enableMusicSync') == "true"):
|
|
||||||
|
|
||||||
if type == "song":
|
|
||||||
connection = utils.KodiSQL('music')
|
|
||||||
cursor = connection.cursor()
|
|
||||||
embyid = ReadKodiDB().getEmbyIdByKodiId(id, type, connection, cursor)
|
|
||||||
cursor.close()
|
|
||||||
else:
|
|
||||||
embyid = ReadKodiDB().getEmbyIdByKodiId(id,type)
|
|
||||||
|
|
||||||
if embyid:
|
|
||||||
|
|
||||||
url = "{server}/mediabrowser/Users/{UserId}/Items/%s?format=json" % embyid
|
|
||||||
result = downloadUtils.downloadUrl(url)
|
|
||||||
self.logMsg("Result: %s" % result, 2)
|
|
||||||
|
|
||||||
playurl = None
|
|
||||||
count = 0
|
|
||||||
while not playurl and count < 2:
|
|
||||||
try:
|
|
||||||
playurl = xbmc.Player().getPlayingFile()
|
|
||||||
except RuntimeError:
|
|
||||||
xbmc.sleep(200)
|
|
||||||
else:
|
|
||||||
listItem = xbmcgui.ListItem()
|
|
||||||
PlaybackUtils().setProperties(playurl, result, listItem)
|
|
||||||
|
|
||||||
if type == "song" and utils.settings('directstreammusic') == "true":
|
|
||||||
utils.window('%splaymethod' % playurl, value="DirectStream")
|
|
||||||
else:
|
|
||||||
utils.window('%splaymethod' % playurl, value="DirectPlay")
|
|
||||||
|
|
||||||
count += 1
|
|
||||||
|
|
||||||
if method == "VideoLibrary.OnUpdate":
|
|
||||||
# Triggers 4 times, the following is only for manually marking as watched/unwatched
|
|
||||||
jsondata = json.loads(data)
|
|
||||||
|
|
||||||
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)
|
|
||||||
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)
|
|
||||||
|
|
||||||
self.clearProperty(type, item)
|
|
||||||
|
|
||||||
if method == "System.OnWake":
|
|
||||||
xbmc.sleep(10000) #Allow network to wake up
|
|
||||||
WINDOW.setProperty("OnWakeSync", "true")
|
|
||||||
|
|
||||||
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 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 == "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")
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,364 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
#################################################################################################
|
|
||||||
|
|
||||||
import xbmc
|
|
||||||
import xbmcgui
|
|
||||||
import xbmcvfs
|
|
||||||
|
|
||||||
from ClientInformation import ClientInformation
|
|
||||||
import Utils as utils
|
|
||||||
|
|
||||||
#################################################################################################
|
|
||||||
|
|
||||||
class PlayUtils():
|
|
||||||
|
|
||||||
clientInfo = ClientInformation()
|
|
||||||
addonName = clientInfo.getAddonName()
|
|
||||||
|
|
||||||
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')
|
|
||||||
|
|
||||||
|
|
||||||
def isDirectPlay(self, result, dialog = False):
|
|
||||||
# Requirements for Direct play:
|
|
||||||
# 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)
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Avoid H265 1080p
|
|
||||||
if (utils.settings('transcodeH265') == "true" and
|
|
||||||
result['MediaSources'][0]['Name'].startswith("1080P/H265")):
|
|
||||||
self.logMsg("Option to transcode 1080P/H265 enabled.", 1)
|
|
||||||
return False
|
|
||||||
|
|
||||||
canDirectPlay = result['MediaSources'][0]['SupportsDirectPlay']
|
|
||||||
# Make sure it's supported by server
|
|
||||||
if not canDirectPlay:
|
|
||||||
self.logMsg("Can't direct play: Server does not allow or 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)
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def directPlay(self, result):
|
|
||||||
|
|
||||||
try:
|
|
||||||
playurl = result['MediaSources'][0]['Path']
|
|
||||||
except KeyError:
|
|
||||||
playurl = result['Path']
|
|
||||||
|
|
||||||
if 'VideoType' in result:
|
|
||||||
# Specific format modification
|
|
||||||
if 'Dvd' in result['VideoType']:
|
|
||||||
playurl = "%s/VIDEO_TS/VIDEO_TS.IFO" % playurl
|
|
||||||
elif 'BluRay' in result['VideoType']:
|
|
||||||
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://")
|
|
||||||
playurl = playurl.replace("\\", "/")
|
|
||||||
|
|
||||||
if "apple.com" in playurl:
|
|
||||||
USER_AGENT = "QuickTime/7.7.4"
|
|
||||||
playurl += "?|User-Agent=%s" % USER_AGENT
|
|
||||||
|
|
||||||
return playurl
|
|
||||||
|
|
||||||
|
|
||||||
def isDirectStream(self, result):
|
|
||||||
# Requirements for Direct stream:
|
|
||||||
# FileSystem or Remote, BitRate, supported encoding
|
|
||||||
|
|
||||||
# Avoid H265 1080p
|
|
||||||
if (utils.settings('transcodeH265') == "true" and
|
|
||||||
result['MediaSources'][0]['Name'].startswith("1080P/H265")):
|
|
||||||
self.logMsg("Option to transcode 1080P/H265 enabled.", 1)
|
|
||||||
return False
|
|
||||||
|
|
||||||
canDirectStream = result['MediaSources'][0]['SupportsDirectStream']
|
|
||||||
# Make sure it's supported by server
|
|
||||||
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)
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def directStream(self, result, server, id, type = "Video"):
|
|
||||||
|
|
||||||
if result['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)
|
|
||||||
|
|
||||||
return playurl
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
try:
|
|
||||||
mediaSources = result['MediaSources']
|
|
||||||
sourceBitRate = int(mediaSources[0]['Bitrate'])
|
|
||||||
except KeyError:
|
|
||||||
self.logMsg("Bitrate value is missing.", 1)
|
|
||||||
else:
|
|
||||||
self.logMsg("The video quality selected is: %s, the video bitrate required to direct stream is: %s." % (settingsVideoBitRate, sourceBitRate), 1)
|
|
||||||
if settingsVideoBitRate < sourceBitRate:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def getVideoBitRate(self):
|
|
||||||
# get the addon video quality
|
|
||||||
videoQuality = utils.settings('videoBitRate')
|
|
||||||
bitrate = {
|
|
||||||
|
|
||||||
'0': 664,
|
|
||||||
'1': 996,
|
|
||||||
'2': 1320,
|
|
||||||
'3': 2000,
|
|
||||||
'4': 3200,
|
|
||||||
'5': 4700,
|
|
||||||
'6': 6200,
|
|
||||||
'7': 7700,
|
|
||||||
'8': 9200,
|
|
||||||
'9': 10700,
|
|
||||||
'10': 12200,
|
|
||||||
'11': 13700,
|
|
||||||
'12': 15200,
|
|
||||||
'13': 16700,
|
|
||||||
'14': 18200,
|
|
||||||
'15': 20000,
|
|
||||||
'16': 40000,
|
|
||||||
'17': 100000,
|
|
||||||
'18': 1000000
|
|
||||||
}
|
|
||||||
|
|
||||||
# max bit rate supported by server (max signed 32bit integer)
|
|
||||||
return bitrate.get(videoQuality, 2147483)
|
|
||||||
|
|
||||||
def 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):
|
|
||||||
# For transcoding only
|
|
||||||
# Present the list of audio to select from
|
|
||||||
audioStreamsList = {}
|
|
||||||
audioStreams = []
|
|
||||||
audioStreamsChannelsList = {}
|
|
||||||
subtitleStreamsList = {}
|
|
||||||
subtitleStreams = ['No subtitles']
|
|
||||||
selectAudioIndex = ""
|
|
||||||
selectSubsIndex = ""
|
|
||||||
playurlprefs = "%s" % url
|
|
||||||
|
|
||||||
mediaStream = mediaSources[0].get('MediaStreams')
|
|
||||||
for stream in mediaStream:
|
|
||||||
# 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']
|
|
||||||
|
|
||||||
try:
|
|
||||||
track = "%s - %s - %s %s" % (index, stream['Language'], codec, channelLayout)
|
|
||||||
except:
|
|
||||||
track = "%s - %s %s" % (index, codec, channelLayout)
|
|
||||||
|
|
||||||
audioStreamsChannelsList[index] = stream['Channels']
|
|
||||||
audioStreamsList[track] = index
|
|
||||||
audioStreams.append(track)
|
|
||||||
|
|
||||||
elif 'Subtitle' in type:
|
|
||||||
try:
|
|
||||||
track = "%s - %s" % (index, stream['Language'])
|
|
||||||
except:
|
|
||||||
track = "%s - %s" % (index, stream['Codec'])
|
|
||||||
|
|
||||||
default = stream['IsDefault']
|
|
||||||
forced = stream['IsForced']
|
|
||||||
if default:
|
|
||||||
track = "%s - Default" % track
|
|
||||||
if forced:
|
|
||||||
track = "%s - Forced" % track
|
|
||||||
|
|
||||||
subtitleStreamsList[track] = index
|
|
||||||
subtitleStreams.append(track)
|
|
||||||
|
|
||||||
|
|
||||||
if len(audioStreams) > 1:
|
|
||||||
resp = xbmcgui.Dialog().select("Choose the audio stream", audioStreams)
|
|
||||||
if resp > -1:
|
|
||||||
# User selected audio
|
|
||||||
selected = audioStreams[resp]
|
|
||||||
selectAudioIndex = audioStreamsList[selected]
|
|
||||||
playurlprefs += "&AudioStreamIndex=%s" % selectAudioIndex
|
|
||||||
else: # User backed out of selection
|
|
||||||
playurlprefs += "&AudioStreamIndex=%s" % mediaSources[0]['DefaultAudioStreamIndex']
|
|
||||||
else: # There's only one audiotrack.
|
|
||||||
selectAudioIndex = audioStreamsList[audioStreams[0]]
|
|
||||||
playurlprefs += "&AudioStreamIndex=%s" % selectAudioIndex
|
|
||||||
|
|
||||||
if len(subtitleStreams) > 1:
|
|
||||||
resp = xbmcgui.Dialog().select("Choose the subtitle stream", subtitleStreams)
|
|
||||||
if resp == 0:
|
|
||||||
# User selected no subtitles
|
|
||||||
pass
|
|
||||||
elif resp > -1:
|
|
||||||
# User selected subtitles
|
|
||||||
selected = subtitleStreams[resp]
|
|
||||||
selectSubsIndex = subtitleStreamsList[selected]
|
|
||||||
playurlprefs += "&SubtitleStreamIndex=%s" % selectSubsIndex
|
|
||||||
else: # User backed out of selection
|
|
||||||
playurlprefs += "&SubtitleStreamIndex=%s" % mediaSources[0].get('DefaultSubtitleStreamIndex', "")
|
|
||||||
|
|
||||||
# Get number of channels for selected audio track
|
|
||||||
audioChannels = audioStreamsChannelsList.get(selectAudioIndex, 0)
|
|
||||||
if audioChannels > 2:
|
|
||||||
playurlprefs += "&AudioBitrate=384000"
|
|
||||||
else:
|
|
||||||
playurlprefs += "&AudioBitrate=192000"
|
|
||||||
|
|
||||||
return playurlprefs
|
|
|
@ -1,462 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
#################################################################################################
|
|
||||||
|
|
||||||
import datetime
|
|
||||||
import json as json
|
|
||||||
import sys
|
|
||||||
|
|
||||||
import xbmc
|
|
||||||
import xbmcaddon
|
|
||||||
import xbmcplugin
|
|
||||||
import xbmcgui
|
|
||||||
|
|
||||||
from API import API
|
|
||||||
from DownloadUtils import DownloadUtils
|
|
||||||
from PlayUtils import PlayUtils
|
|
||||||
from ClientInformation import ClientInformation
|
|
||||||
import Utils as utils
|
|
||||||
|
|
||||||
#################################################################################################
|
|
||||||
|
|
||||||
class PlaybackUtils():
|
|
||||||
|
|
||||||
clientInfo = ClientInformation()
|
|
||||||
doUtils = DownloadUtils()
|
|
||||||
api = API()
|
|
||||||
|
|
||||||
addon = xbmcaddon.Addon()
|
|
||||||
language = addon.getLocalizedString
|
|
||||||
addonName = clientInfo.getAddonName()
|
|
||||||
|
|
||||||
def logMsg(self, msg, lvl=1):
|
|
||||||
|
|
||||||
className = self.__class__.__name__
|
|
||||||
utils.logMsg("%s %s" % (self.addonName, className), msg, int(lvl))
|
|
||||||
|
|
||||||
def PLAY(self, result, setup = "service"):
|
|
||||||
|
|
||||||
self.logMsg("PLAY Called", 1)
|
|
||||||
|
|
||||||
api = self.api
|
|
||||||
doUtils = self.doUtils
|
|
||||||
username = utils.window('currUser')
|
|
||||||
server = utils.window('server%s' % username)
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
############### ORGANIZE CURRENT PLAYLIST ################
|
|
||||||
|
|
||||||
homeScreen = xbmc.getCondVisibility('Window.IsActive(home)')
|
|
||||||
playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
|
|
||||||
startPos = max(playlist.getposition(), 0) # Can return -1
|
|
||||||
sizePlaylist = playlist.size()
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
############### 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)
|
|
||||||
|
|
||||||
# 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.")
|
|
||||||
|
|
||||||
############### -- CHECK FOR INTROS ################
|
|
||||||
|
|
||||||
if utils.settings('disableCinema') == "false" 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
|
|
||||||
intros = doUtils.downloadUrl(url)
|
|
||||||
|
|
||||||
if intros['TotalRecordCount'] != 0:
|
|
||||||
getTrailers = True
|
|
||||||
|
|
||||||
if utils.settings('askCinema') == "true":
|
|
||||||
resp = xbmcgui.Dialog().yesno("Emby Cinema Mode", "Play trailers?")
|
|
||||||
if not resp:
|
|
||||||
# User selected to not play trailers
|
|
||||||
getTrailers = False
|
|
||||||
self.logMsg("Skip trailers.", 1)
|
|
||||||
|
|
||||||
if getTrailers:
|
|
||||||
for intro in intros['Items']:
|
|
||||||
# The server randomly returns intros, process them.
|
|
||||||
introId = intro['Id']
|
|
||||||
|
|
||||||
introPlayurl = PlayUtils().getPlayUrl(server, introId, intro)
|
|
||||||
introListItem = xbmcgui.ListItem()
|
|
||||||
self.logMsg("Adding Intro: %s" % introPlayurl, 1)
|
|
||||||
|
|
||||||
# Set listitem and properties for intros
|
|
||||||
self.setProperties(introPlayurl, intro, introListItem)
|
|
||||||
self.setListItemProps(server, introId, introListItem, intro)
|
|
||||||
|
|
||||||
playlist.add(introPlayurl, introListItem, index=currentPosition)
|
|
||||||
introsPlaylist = True
|
|
||||||
currentPosition += 1
|
|
||||||
|
|
||||||
|
|
||||||
############### -- ADD MAIN ITEM ONLY FOR HOMESCREEN ###############
|
|
||||||
|
|
||||||
if homeScreen and not sizePlaylist:
|
|
||||||
# Extend our current playlist with the actual item to play only if there's no playlist first
|
|
||||||
self.logMsg("Adding main item to playlist.", 1)
|
|
||||||
self.setListItemProps(server, id, listItem, result)
|
|
||||||
playlist.add(playurl, listItem, index=currentPosition)
|
|
||||||
|
|
||||||
# Ensure that additional parts are played after the main item
|
|
||||||
currentPosition += 1
|
|
||||||
|
|
||||||
|
|
||||||
############### -- CHECK FOR ADDITIONAL PARTS ################
|
|
||||||
|
|
||||||
if result.get('PartCount'):
|
|
||||||
# Only add to the playlist after intros have played
|
|
||||||
partcount = result['PartCount']
|
|
||||||
url = "{server}/mediabrowser/Videos/%s/AdditionalParts" % id
|
|
||||||
parts = doUtils.downloadUrl(url)
|
|
||||||
|
|
||||||
for part in parts['Items']:
|
|
||||||
|
|
||||||
partId = part['Id']
|
|
||||||
additionalPlayurl = PlayUtils().getPlayUrl(server, partId, part)
|
|
||||||
additionalListItem = xbmcgui.ListItem()
|
|
||||||
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)
|
|
||||||
|
|
||||||
playlist.add(additionalPlayurl, additionalListItem, index=currentPosition)
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
# We just skipped adding properties. Reset flag for next time.
|
|
||||||
elif propertiesPlayback:
|
|
||||||
self.logMsg("Resetting properties playback flag.", 2)
|
|
||||||
utils.window('propertiesPlayback', clear=True)
|
|
||||||
|
|
||||||
|
|
||||||
self.verifyPlaylist()
|
|
||||||
|
|
||||||
############### PLAYBACK ################
|
|
||||||
|
|
||||||
if not homeScreen and not introsPlaylist:
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
else:
|
|
||||||
self.logMsg("Play as a regular item.", 1)
|
|
||||||
xbmc.Player().play(playlist, startpos=startPos)
|
|
||||||
|
|
||||||
|
|
||||||
def verifyPlaylist(self):
|
|
||||||
|
|
||||||
playlistitems = '{"jsonrpc": "2.0", "method": "Playlist.GetItems", "params": { "playlistid": 1 }, "id": 1}'
|
|
||||||
items = xbmc.executeJSONRPC(playlistitems)
|
|
||||||
self.logMsg(items, 2)
|
|
||||||
|
|
||||||
def removeFromPlaylist(self, pos):
|
|
||||||
|
|
||||||
playlistremove = '{"jsonrpc": "2.0", "method": "Playlist.Remove", "params": { "playlistid": 1, "position": %d }, "id": 1}' % pos
|
|
||||||
result = xbmc.executeJSONRPC(playlistremove)
|
|
||||||
self.logMsg(result, 1)
|
|
||||||
|
|
||||||
|
|
||||||
def externalSubs(self, id, playurl, mediaSources):
|
|
||||||
|
|
||||||
username = utils.window('currUser')
|
|
||||||
server = utils.window('server%s' % username)
|
|
||||||
externalsubs = []
|
|
||||||
mapping = {}
|
|
||||||
|
|
||||||
mediaStream = mediaSources[0].get('MediaStreams')
|
|
||||||
kodiindex = 0
|
|
||||||
for stream in mediaStream:
|
|
||||||
|
|
||||||
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']:
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
# map external subtitles for mapping
|
|
||||||
mapping[kodiindex] = index
|
|
||||||
externalsubs.append(url)
|
|
||||||
kodiindex += 1
|
|
||||||
|
|
||||||
mapping = json.dumps(mapping)
|
|
||||||
utils.window('%sIndexMapping' % 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):
|
|
||||||
# Set up item and item info
|
|
||||||
api = self.api
|
|
||||||
|
|
||||||
type = result.get('Type')
|
|
||||||
people = api.getPeople(result)
|
|
||||||
studios = api.getStudios(result)
|
|
||||||
|
|
||||||
metadata = {
|
|
||||||
|
|
||||||
'title': result.get('Name', "Missing name"),
|
|
||||||
'year': result.get('ProductionYear'),
|
|
||||||
'plot': api.getOverview(result),
|
|
||||||
'director': people.get('Director'),
|
|
||||||
'writer': people.get('Writer'),
|
|
||||||
'mpaa': api.getMpaa(result),
|
|
||||||
'genre': api.getGenre(result),
|
|
||||||
'studio': " / ".join(studios),
|
|
||||||
'aired': api.getPremiereDate(result),
|
|
||||||
'rating': result.get('CommunityRating'),
|
|
||||||
'votes': result.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', "")
|
|
||||||
|
|
||||||
metadata['TVShowTitle'] = show
|
|
||||||
metadata['season'] = season
|
|
||||||
metadata['episode'] = episode
|
|
||||||
|
|
||||||
listItem.setProperty('IsPlayable', 'true')
|
|
||||||
listItem.setProperty('IsFolder', 'false')
|
|
||||||
listItem.setLabel(metadata['title'])
|
|
||||||
listItem.setInfo('video', infoLabels=metadata)
|
|
||||||
|
|
||||||
# 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)'''
|
|
|
@ -1,440 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
#################################################################################################
|
|
||||||
|
|
||||||
import json as 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
|
|
||||||
|
|
||||||
#################################################################################################
|
|
||||||
|
|
||||||
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 = {}
|
|
||||||
playStats = {}
|
|
||||||
currentFile = None
|
|
||||||
|
|
||||||
def __init__(self, *args):
|
|
||||||
|
|
||||||
self.__dict__ = self._shared_state
|
|
||||||
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))
|
|
||||||
|
|
||||||
def GetPlayStats(self):
|
|
||||||
return self.playStats
|
|
||||||
|
|
||||||
def onPlayBackStarted( self ):
|
|
||||||
# Will be called when xbmc starts playing a file
|
|
||||||
xbmcplayer = self.xbmcplayer
|
|
||||||
self.stopAll()
|
|
||||||
|
|
||||||
# Get current file
|
|
||||||
try:
|
|
||||||
currentFile = xbmcplayer.getPlayingFile()
|
|
||||||
xbmc.sleep(300)
|
|
||||||
except:
|
|
||||||
currentFile = ""
|
|
||||||
count = 0
|
|
||||||
while not currentFile:
|
|
||||||
xbmc.sleep(100)
|
|
||||||
try:
|
|
||||||
currentFile = xbmcplayer.getPlayingFile()
|
|
||||||
except: pass
|
|
||||||
|
|
||||||
if count == 5: # try 5 times
|
|
||||||
self.logMsg("Cancelling playback report...", 1)
|
|
||||||
break
|
|
||||||
else: count += 1
|
|
||||||
|
|
||||||
|
|
||||||
if currentFile:
|
|
||||||
|
|
||||||
self.currentFile = currentFile
|
|
||||||
|
|
||||||
# We may need to wait for info to be set in kodi monitor
|
|
||||||
itemId = utils.window("%sitem_id" % currentFile)
|
|
||||||
tryCount = 0
|
|
||||||
while not itemId:
|
|
||||||
|
|
||||||
xbmc.sleep(200)
|
|
||||||
itemId = utils.window("%sitem_id" % currentFile)
|
|
||||||
if tryCount == 20: # try 20 times or about 10 seconds
|
|
||||||
self.logMsg("Could not find itemId, cancelling playback report...", 1)
|
|
||||||
break
|
|
||||||
else: tryCount += 1
|
|
||||||
|
|
||||||
else:
|
|
||||||
self.logMsg("ONPLAYBACK_STARTED: %s ITEMID: %s" % (currentFile, itemId), 0)
|
|
||||||
|
|
||||||
# Only proceed if an itemId was found.
|
|
||||||
runtime = utils.window("%sruntimeticks" % currentFile)
|
|
||||||
refresh_id = utils.window("%srefresh_id" % currentFile)
|
|
||||||
playMethod = utils.window("%splaymethod" % currentFile)
|
|
||||||
itemType = utils.window("%stype" % currentFile)
|
|
||||||
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)
|
|
||||||
result = json.loads(result)
|
|
||||||
result = result.get('result')
|
|
||||||
|
|
||||||
volume = result.get('volume')
|
|
||||||
muted = result.get('muted')
|
|
||||||
|
|
||||||
# Postdata structure to send to Emby server
|
|
||||||
url = "{server}/mediabrowser/Sessions/Playing"
|
|
||||||
postdata = {
|
|
||||||
|
|
||||||
'QueueableMediaTypes': "Video",
|
|
||||||
'CanSeek': True,
|
|
||||||
'ItemId': itemId,
|
|
||||||
'MediaSourceId': itemId,
|
|
||||||
'PlayMethod': playMethod,
|
|
||||||
'VolumeLevel': volume,
|
|
||||||
'PositionTicks': int(seekTime * 10000000),
|
|
||||||
'IsMuted': muted
|
|
||||||
}
|
|
||||||
|
|
||||||
# Get the current audio track and subtitles
|
|
||||||
if playMethod == "Transcode":
|
|
||||||
# property set in PlayUtils.py
|
|
||||||
postdata['AudioStreamIndex'] = utils.window("%sAudioStreamIndex" % currentFile)
|
|
||||||
postdata['SubtitleStreamIndex'] = utils.window("%sSubtitleStreamIndex" % currentFile)
|
|
||||||
|
|
||||||
else:
|
|
||||||
# Get the current kodi audio and subtitles and convert to Emby equivalent
|
|
||||||
track_query = '{"jsonrpc": "2.0", "method": "Player.GetProperties", "params": {"playerid": 1,"properties": ["currentsubtitle","currentaudiostream","subtitleenabled"]} , "id": 1}'
|
|
||||||
result = xbmc.executeJSONRPC(track_query)
|
|
||||||
result = json.loads(result)
|
|
||||||
result = result.get('result')
|
|
||||||
|
|
||||||
try: # Audio tracks
|
|
||||||
indexAudio = result['currentaudiostream']['index']
|
|
||||||
except (KeyError, TypeError):
|
|
||||||
indexAudio = 0
|
|
||||||
|
|
||||||
try: # Subtitles tracks
|
|
||||||
indexSubs = result['currentsubtitle']['index']
|
|
||||||
except (KeyError, TypeError):
|
|
||||||
indexSubs = 0
|
|
||||||
|
|
||||||
try: # If subtitles are enabled
|
|
||||||
subsEnabled = result['subtitleenabled']
|
|
||||||
except (KeyError, TypeError):
|
|
||||||
subsEnabled = ""
|
|
||||||
|
|
||||||
# Postdata for the audio
|
|
||||||
postdata['AudioStreamIndex'] = indexAudio + 1
|
|
||||||
|
|
||||||
# Postdata for the subtitles
|
|
||||||
if subsEnabled and len(xbmc.Player().getAvailableSubtitleStreams()) > 0:
|
|
||||||
|
|
||||||
# Number of audiotracks to help get Emby Index
|
|
||||||
audioTracks = len(xbmc.Player().getAvailableAudioStreams())
|
|
||||||
mapping = utils.window("%sIndexMapping" % currentFile)
|
|
||||||
|
|
||||||
if mapping: # Set in PlaybackUtils.py
|
|
||||||
|
|
||||||
self.logMsg("Mapping for external subtitles index: %s" % mapping, 2)
|
|
||||||
externalIndex = json.loads(mapping)
|
|
||||||
|
|
||||||
if externalIndex.get(str(indexSubs)):
|
|
||||||
# If the current subtitle is in the mapping
|
|
||||||
postdata['SubtitleStreamIndex'] = externalIndex[str(indexSubs)]
|
|
||||||
else:
|
|
||||||
# Internal subtitle currently selected
|
|
||||||
postdata['SubtitleStreamIndex'] = indexSubs - len(externalIndex) + audioTracks + 1
|
|
||||||
|
|
||||||
else: # Direct paths enabled scenario or no external subtitles set
|
|
||||||
postdata['SubtitleStreamIndex'] = indexSubs + audioTracks + 1
|
|
||||||
else:
|
|
||||||
postdata['SubtitleStreamIndex'] = ""
|
|
||||||
|
|
||||||
|
|
||||||
# Post playback to server
|
|
||||||
self.logMsg("Sending POST play started: %s." % postdata, 2)
|
|
||||||
self.doUtils.downloadUrl(url, postBody=postdata, type="POST")
|
|
||||||
|
|
||||||
# Ensure we do have a runtime
|
|
||||||
try:
|
|
||||||
runtime = int(runtime)
|
|
||||||
except ValueError:
|
|
||||||
runtime = xbmcplayer.getTotalTime()
|
|
||||||
self.logMsg("Runtime is missing, grabbing runtime from Kodi player: %s" % runtime, 1)
|
|
||||||
|
|
||||||
# Save data map for updates and position calls
|
|
||||||
data = {
|
|
||||||
|
|
||||||
'runtime': runtime,
|
|
||||||
'item_id': itemId,
|
|
||||||
'refresh_id': refresh_id,
|
|
||||||
'currentfile': currentFile,
|
|
||||||
'AudioStreamIndex': postdata['AudioStreamIndex'],
|
|
||||||
'SubtitleStreamIndex': postdata['SubtitleStreamIndex'],
|
|
||||||
'playmethod': playMethod,
|
|
||||||
'Type': itemType,
|
|
||||||
'currentPosition': int(seekTime)
|
|
||||||
}
|
|
||||||
|
|
||||||
self.played_information[currentFile] = data
|
|
||||||
self.logMsg("ADDING_FILE: %s" % self.played_information, 1)
|
|
||||||
|
|
||||||
# log some playback stats
|
|
||||||
'''if(itemType != None):
|
|
||||||
if(self.playStats.get(itemType) != None):
|
|
||||||
count = self.playStats.get(itemType) + 1
|
|
||||||
self.playStats[itemType] = count
|
|
||||||
else:
|
|
||||||
self.playStats[itemType] = 1
|
|
||||||
|
|
||||||
if(playMethod != None):
|
|
||||||
if(self.playStats.get(playMethod) != None):
|
|
||||||
count = self.playStats.get(playMethod) + 1
|
|
||||||
self.playStats[playMethod] = count
|
|
||||||
else:
|
|
||||||
self.playStats[playMethod] = 1'''
|
|
||||||
|
|
||||||
def reportPlayback(self):
|
|
||||||
|
|
||||||
self.logMsg("reportPlayback Called", 2)
|
|
||||||
xbmcplayer = self.xbmcplayer
|
|
||||||
|
|
||||||
# Get current file
|
|
||||||
currentFile = self.currentFile
|
|
||||||
data = self.played_information.get(currentFile)
|
|
||||||
|
|
||||||
# only report playback if emby has initiated the playback (item_id has value)
|
|
||||||
if data:
|
|
||||||
# Get playback information
|
|
||||||
itemId = data['item_id']
|
|
||||||
audioindex = data['AudioStreamIndex']
|
|
||||||
subtitleindex = data['SubtitleStreamIndex']
|
|
||||||
playTime = data['currentPosition']
|
|
||||||
playMethod = data['playmethod']
|
|
||||||
paused = data.get('paused', False)
|
|
||||||
|
|
||||||
|
|
||||||
# Get playback volume
|
|
||||||
volume_query = '{"jsonrpc": "2.0", "method": "Application.GetProperties", "params": {"properties": ["volume","muted"]}, "id": 1}'
|
|
||||||
result = xbmc.executeJSONRPC(volume_query)
|
|
||||||
result = json.loads(result)
|
|
||||||
result = result.get('result')
|
|
||||||
|
|
||||||
volume = result.get('volume')
|
|
||||||
muted = result.get('muted')
|
|
||||||
|
|
||||||
|
|
||||||
# Postdata for the websocketclient report
|
|
||||||
postdata = {
|
|
||||||
|
|
||||||
'QueueableMediaTypes': "Video",
|
|
||||||
'CanSeek': True,
|
|
||||||
'ItemId': itemId,
|
|
||||||
'MediaSourceId': itemId,
|
|
||||||
'PlayMethod': playMethod,
|
|
||||||
'PositionTicks': int(playTime * 10000000),
|
|
||||||
'IsPaused': paused,
|
|
||||||
'VolumeLevel': volume,
|
|
||||||
'IsMuted': muted
|
|
||||||
}
|
|
||||||
|
|
||||||
if playMethod == "Transcode":
|
|
||||||
# Track can't be changed, keep reporting the same index
|
|
||||||
postdata['AudioStreamIndex'] = audioindex
|
|
||||||
postdata['AudioStreamIndex'] = subtitleindex
|
|
||||||
|
|
||||||
else:
|
|
||||||
# Get current audio and subtitles track
|
|
||||||
track_query = '{"jsonrpc": "2.0", "method": "Player.GetProperties", "params": {"playerid":1,"properties": ["currentsubtitle","currentaudiostream","subtitleenabled"]} , "id": 1}'
|
|
||||||
result = xbmc.executeJSONRPC(track_query)
|
|
||||||
result = json.loads(result)
|
|
||||||
result = result.get('result')
|
|
||||||
|
|
||||||
try: # Audio tracks
|
|
||||||
indexAudio = result['currentaudiostream']['index']
|
|
||||||
except (KeyError, TypeError):
|
|
||||||
indexAudio = 0
|
|
||||||
|
|
||||||
try: # Subtitles tracks
|
|
||||||
indexSubs = result['currentsubtitle']['index']
|
|
||||||
except (KeyError, TypeError):
|
|
||||||
indexSubs = 0
|
|
||||||
|
|
||||||
try: # If subtitles are enabled
|
|
||||||
subsEnabled = result['subtitleenabled']
|
|
||||||
except (KeyError, TypeError):
|
|
||||||
subsEnabled = ""
|
|
||||||
|
|
||||||
# Postdata for the audio
|
|
||||||
data['AudioStreamIndex'], postdata['AudioStreamIndex'] = [indexAudio + 1] * 2
|
|
||||||
|
|
||||||
# Postdata for the subtitles
|
|
||||||
if subsEnabled and len(xbmc.Player().getAvailableSubtitleStreams()) > 0:
|
|
||||||
|
|
||||||
# Number of audiotracks to help get Emby Index
|
|
||||||
audioTracks = len(xbmc.Player().getAvailableAudioStreams())
|
|
||||||
mapping = utils.window("%sIndexMapping" % currentFile)
|
|
||||||
|
|
||||||
if mapping: # Set in PlaybackUtils.py
|
|
||||||
|
|
||||||
self.logMsg("Mapping for external subtitles index: %s" % mapping, 2)
|
|
||||||
externalIndex = json.loads(mapping)
|
|
||||||
|
|
||||||
if externalIndex.get(str(indexSubs)):
|
|
||||||
# If the current subtitle is in the mapping
|
|
||||||
data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = [externalIndex[str(indexSubs)]] * 2
|
|
||||||
else:
|
|
||||||
# Internal subtitle currently selected
|
|
||||||
data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = [indexSubs - len(externalIndex) + audioTracks + 1] * 2
|
|
||||||
|
|
||||||
else: # Direct paths enabled scenario or no external subtitles set
|
|
||||||
data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = [indexSubs + audioTracks + 1] * 2
|
|
||||||
else:
|
|
||||||
data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = [""] * 2
|
|
||||||
|
|
||||||
# Report progress via websocketclient
|
|
||||||
postdata = json.dumps(postdata)
|
|
||||||
self.logMsg("Report: %s" % postdata, 2)
|
|
||||||
self.ws.sendProgressUpdate(postdata)
|
|
||||||
|
|
||||||
def onPlayBackPaused( self ):
|
|
||||||
|
|
||||||
currentFile = self.currentFile
|
|
||||||
self.logMsg("PLAYBACK_PAUSED: %s" % currentFile, 2)
|
|
||||||
|
|
||||||
if self.played_information.get(currentFile):
|
|
||||||
self.played_information[currentFile]['paused'] = True
|
|
||||||
|
|
||||||
self.reportPlayback()
|
|
||||||
|
|
||||||
def onPlayBackResumed( self ):
|
|
||||||
|
|
||||||
currentFile = self.currentFile
|
|
||||||
self.logMsg("PLAYBACK_RESUMED: %s" % currentFile, 2)
|
|
||||||
|
|
||||||
if self.played_information.get(currentFile):
|
|
||||||
self.played_information[currentFile]['paused'] = False
|
|
||||||
|
|
||||||
self.reportPlayback()
|
|
||||||
|
|
||||||
def onPlayBackSeek( self, time, seekOffset ):
|
|
||||||
# Make position when seeking a bit more accurate
|
|
||||||
currentFile = self.currentFile
|
|
||||||
self.logMsg("PLAYBACK_SEEK: %s" % currentFile, 2)
|
|
||||||
|
|
||||||
if self.played_information.get(currentFile):
|
|
||||||
position = self.xbmcplayer.getTime()
|
|
||||||
self.played_information[currentFile]['currentPosition'] = position
|
|
||||||
|
|
||||||
self.reportPlayback()
|
|
||||||
|
|
||||||
def onPlayBackStopped( self ):
|
|
||||||
# Will be called when user stops xbmc playing a file
|
|
||||||
self.logMsg("ONPLAYBACK_STOPPED", 2)
|
|
||||||
self.stopAll()
|
|
||||||
|
|
||||||
def onPlayBackEnded( self ):
|
|
||||||
# Will be called when xbmc stops playing a file
|
|
||||||
self.logMsg("ONPLAYBACK_ENDED", 2)
|
|
||||||
self.stopAll()
|
|
||||||
|
|
||||||
def stopAll(self):
|
|
||||||
|
|
||||||
if not self.played_information:
|
|
||||||
return
|
|
||||||
|
|
||||||
self.logMsg("Played_information: %s" % self.played_information, 1)
|
|
||||||
# Process each items
|
|
||||||
for item in self.played_information:
|
|
||||||
|
|
||||||
data = self.played_information.get(item)
|
|
||||||
if data:
|
|
||||||
|
|
||||||
self.logMsg("Item path: %s" % item, 2)
|
|
||||||
self.logMsg("Item data: %s" % data, 2)
|
|
||||||
|
|
||||||
runtime = data['runtime']
|
|
||||||
currentPosition = data['currentPosition']
|
|
||||||
itemId = data['item_id']
|
|
||||||
refresh_id = data['refresh_id']
|
|
||||||
currentFile = data['currentfile']
|
|
||||||
type = data['Type']
|
|
||||||
playMethod = data['playmethod']
|
|
||||||
|
|
||||||
if currentPosition and runtime:
|
|
||||||
percentComplete = (currentPosition * 10000000) / int(runtime)
|
|
||||||
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 > WriteKodiVideoDB().UpdatePlaycountFromKodi()
|
|
||||||
utils.window('SkipWatched%s' % itemId, "true")
|
|
||||||
|
|
||||||
self.stopPlayback(data)
|
|
||||||
offerDelete = utils.settings('offerDelete') == "true"
|
|
||||||
offerTypeDelete = False
|
|
||||||
|
|
||||||
if type == "Episode" and utils.settings('offerDeleteTV') == "true":
|
|
||||||
offerTypeDelete = True
|
|
||||||
|
|
||||||
elif type == "Movie" and utils.settings('offerDeleteMovies') == "true":
|
|
||||||
offerTypeDelete = 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)
|
|
||||||
|
|
||||||
# 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")
|
|
||||||
|
|
||||||
self.played_information.clear()
|
|
||||||
|
|
||||||
def stopPlayback(self, data):
|
|
||||||
|
|
||||||
self.logMsg("stopPlayback called", 2)
|
|
||||||
|
|
||||||
itemId = data['item_id']
|
|
||||||
currentPosition = data['currentPosition']
|
|
||||||
positionTicks = int(currentPosition * 10000000)
|
|
||||||
|
|
||||||
url = "{server}/mediabrowser/Sessions/Playing/Stopped"
|
|
||||||
postdata = {
|
|
||||||
|
|
||||||
'ItemId': itemId,
|
|
||||||
'MediaSourceId': itemId,
|
|
||||||
'PositionTicks': positionTicks
|
|
||||||
}
|
|
||||||
|
|
||||||
self.doUtils.downloadUrl(url, postBody=postdata, type="POST")
|
|
|
@ -1,460 +0,0 @@
|
||||||
#################################################################################################
|
|
||||||
# UserClient thread
|
|
||||||
#################################################################################################
|
|
||||||
|
|
||||||
import xbmc
|
|
||||||
import xbmcgui
|
|
||||||
import xbmcaddon
|
|
||||||
import xbmcvfs
|
|
||||||
|
|
||||||
import threading
|
|
||||||
import hashlib
|
|
||||||
import json as json
|
|
||||||
|
|
||||||
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):
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
currUser = None
|
|
||||||
currUserId = None
|
|
||||||
currServer = None
|
|
||||||
currToken = None
|
|
||||||
HasAccess = True
|
|
||||||
AdditionalUser = []
|
|
||||||
|
|
||||||
def __init__(self, *args):
|
|
||||||
|
|
||||||
self.__dict__ = self._shared_state
|
|
||||||
threading.Thread.__init__(self, *args)
|
|
||||||
|
|
||||||
def logMsg(self, msg, lvl=1):
|
|
||||||
|
|
||||||
className = self.__class__.__name__
|
|
||||||
utils.logMsg("%s %s" % (self.addonName, className), str(msg), int(lvl))
|
|
||||||
|
|
||||||
def getUsername(self):
|
|
||||||
|
|
||||||
username = utils.settings('username')
|
|
||||||
|
|
||||||
if (username == ""):
|
|
||||||
self.logMsg("No username saved.", 2)
|
|
||||||
return ""
|
|
||||||
|
|
||||||
return username
|
|
||||||
|
|
||||||
def getAdditionalUsers(self):
|
|
||||||
|
|
||||||
additionalUsers = utils.settings('additionalUsers')
|
|
||||||
|
|
||||||
if additionalUsers:
|
|
||||||
self.AdditionalUser = additionalUsers.split(',')
|
|
||||||
|
|
||||||
def getLogLevel(self):
|
|
||||||
|
|
||||||
try:
|
|
||||||
logLevel = int(utils.settings('logLevel'))
|
|
||||||
except:
|
|
||||||
logLevel = 0
|
|
||||||
|
|
||||||
return logLevel
|
|
||||||
|
|
||||||
def getUserId(self):
|
|
||||||
|
|
||||||
username = self.getUsername()
|
|
||||||
w_userId = self.WINDOW.getProperty('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)
|
|
||||||
return w_userId
|
|
||||||
# Verify the settings
|
|
||||||
elif (s_userId != ""):
|
|
||||||
self.logMsg("Returning userId from SETTINGS for username: %s userId: %s" % (username, s_userId), 2)
|
|
||||||
return s_userId
|
|
||||||
# No userId found
|
|
||||||
else:
|
|
||||||
self.logMsg("No userId saved for username: %s." % username)
|
|
||||||
return
|
|
||||||
|
|
||||||
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')
|
|
||||||
host = utils.settings('secondipaddress')
|
|
||||||
port = utils.settings('secondport')
|
|
||||||
|
|
||||||
server = host + ":" + port
|
|
||||||
|
|
||||||
if host == "":
|
|
||||||
self.logMsg("No server information saved.", 2)
|
|
||||||
return ""
|
|
||||||
|
|
||||||
# If https is true
|
|
||||||
if prefix and (HTTPS == "true"):
|
|
||||||
server = "https://%s" % server
|
|
||||||
return server
|
|
||||||
# If https is false
|
|
||||||
elif prefix and (HTTPS == "false"):
|
|
||||||
server = "http://%s" % server
|
|
||||||
return server
|
|
||||||
# If only the host:port is required
|
|
||||||
elif (prefix == False):
|
|
||||||
return server
|
|
||||||
|
|
||||||
def getToken(self):
|
|
||||||
|
|
||||||
username = self.getUsername()
|
|
||||||
w_token = self.WINDOW.getProperty('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)
|
|
||||||
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)
|
|
||||||
return s_token
|
|
||||||
else:
|
|
||||||
self.logMsg("No token found.")
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def getSSLverify(self):
|
|
||||||
# Verify host certificate
|
|
||||||
s_sslverify = utils.settings('sslverify')
|
|
||||||
if utils.settings('altip') == "true":
|
|
||||||
s_sslverify = utils.settings('secondsslverify')
|
|
||||||
|
|
||||||
if s_sslverify == "true":
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def getSSL(self):
|
|
||||||
# Client side certificate
|
|
||||||
s_cert = utils.settings('sslcert')
|
|
||||||
if utils.settings('altip') == "true":
|
|
||||||
s_cert = utils.settings('secondsslcert')
|
|
||||||
|
|
||||||
if s_cert == "None":
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
return s_cert
|
|
||||||
|
|
||||||
def setUserPref(self):
|
|
||||||
|
|
||||||
player = Player()
|
|
||||||
server = self.getServer()
|
|
||||||
userId = self.getUserId()
|
|
||||||
|
|
||||||
url = "{server}/mediabrowser/Users/{UserId}?format=json"
|
|
||||||
result = self.doUtils.downloadUrl(url)
|
|
||||||
|
|
||||||
# Set user image for skin display
|
|
||||||
self.WINDOW.setProperty("EmbyUserImage",API().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)
|
|
||||||
|
|
||||||
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
|
|
||||||
result = self.doUtils.downloadUrl(url, authenticate=False)
|
|
||||||
|
|
||||||
users = []
|
|
||||||
|
|
||||||
if (result != ""):
|
|
||||||
users = result
|
|
||||||
else:
|
|
||||||
# Server connection failed
|
|
||||||
return False
|
|
||||||
|
|
||||||
return users
|
|
||||||
|
|
||||||
def hasAccess(self):
|
|
||||||
|
|
||||||
url = "{server}/mediabrowser/Users"
|
|
||||||
result = self.doUtils.downloadUrl(url)
|
|
||||||
|
|
||||||
if result is False:
|
|
||||||
# Access is restricted
|
|
||||||
self.logMsg("Access is restricted.")
|
|
||||||
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.")
|
|
||||||
self.HasAccess = True
|
|
||||||
self.WINDOW.setProperty("Server_status", "")
|
|
||||||
xbmcgui.Dialog().notification("Emby server", "Access is enabled.")
|
|
||||||
return
|
|
||||||
|
|
||||||
def loadCurrUser(self, authenticated=False):
|
|
||||||
|
|
||||||
WINDOW = self.WINDOW
|
|
||||||
doUtils = self.doUtils
|
|
||||||
username = self.getUsername()
|
|
||||||
|
|
||||||
# Only to be used if token exists
|
|
||||||
self.currUserId = self.getUserId()
|
|
||||||
self.currServer = self.getServer()
|
|
||||||
self.currToken = self.getToken()
|
|
||||||
self.ssl = self.getSSLverify()
|
|
||||||
self.sslcert = self.getSSL()
|
|
||||||
|
|
||||||
# Test the validity of current token
|
|
||||||
if authenticated == False:
|
|
||||||
url = "%s/mediabrowser/Users/%s" % (self.currServer, self.currUserId)
|
|
||||||
WINDOW.setProperty("currUser", username)
|
|
||||||
WINDOW.setProperty("accessToken%s" % username, 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)
|
|
||||||
|
|
||||||
# Set DownloadUtils values
|
|
||||||
doUtils.setUsername(username)
|
|
||||||
doUtils.setUserId(self.currUserId)
|
|
||||||
doUtils.setServer(self.currServer)
|
|
||||||
doUtils.setToken(self.currToken)
|
|
||||||
doUtils.setSSL(self.ssl, self.sslcert)
|
|
||||||
# parental control - let's verify if access is restricted
|
|
||||||
self.hasAccess()
|
|
||||||
# Start DownloadUtils session
|
|
||||||
doUtils.startSession()
|
|
||||||
self.getAdditionalUsers()
|
|
||||||
|
|
||||||
self.currUser = username
|
|
||||||
# Set user preferences in settings
|
|
||||||
self.setUserPref()
|
|
||||||
|
|
||||||
def authenticate(self):
|
|
||||||
|
|
||||||
WINDOW = self.WINDOW
|
|
||||||
addon = self.addon
|
|
||||||
|
|
||||||
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.")
|
|
||||||
self.auth = False
|
|
||||||
return
|
|
||||||
# If no user information
|
|
||||||
if (server == "") or (username == ""):
|
|
||||||
self.logMsg("Missing server information.")
|
|
||||||
self.auth = False
|
|
||||||
return
|
|
||||||
# If there's a token
|
|
||||||
if (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)
|
|
||||||
return
|
|
||||||
|
|
||||||
users = self.getPublicUsers()
|
|
||||||
password = ""
|
|
||||||
|
|
||||||
# Find user in list
|
|
||||||
for user in users:
|
|
||||||
name = user[u'Name']
|
|
||||||
userHasPassword = False
|
|
||||||
|
|
||||||
if (unicode(username, 'utf-8') in name):
|
|
||||||
# Verify if user has a password
|
|
||||||
if (user.get("HasPassword") == True):
|
|
||||||
userHasPassword = True
|
|
||||||
# If user has password
|
|
||||||
if (userHasPassword):
|
|
||||||
password = xbmcgui.Dialog().input("Enter password for user: %s" % username, option=xbmcgui.ALPHANUM_HIDE_INPUT)
|
|
||||||
# If password dialog is cancelled
|
|
||||||
if (password == ""):
|
|
||||||
self.logMsg("No password entered.", 0)
|
|
||||||
self.WINDOW.setProperty("Server_status", "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)
|
|
||||||
|
|
||||||
sha1 = hashlib.sha1(password)
|
|
||||||
sha1 = sha1.hexdigest()
|
|
||||||
|
|
||||||
# Authenticate username and password
|
|
||||||
url = "%s/mediabrowser/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
|
|
||||||
|
|
||||||
if (result != None and accessToken != 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)
|
|
||||||
self.loadCurrUser(authenticated=True)
|
|
||||||
self.WINDOW.setProperty("Server_status", "")
|
|
||||||
self.retry = 0
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
self.logMsg("User authentication failed.")
|
|
||||||
utils.settings("accessToken", "")
|
|
||||||
utils.settings("userId%s" % username, "")
|
|
||||||
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.auth = False
|
|
||||||
return
|
|
||||||
|
|
||||||
def resetClient(self):
|
|
||||||
|
|
||||||
username = self.getUsername()
|
|
||||||
self.logMsg("Reset UserClient authentication.", 1)
|
|
||||||
if (self.currToken != None):
|
|
||||||
# In case of 401, removed saved token
|
|
||||||
utils.settings("accessToken", "")
|
|
||||||
self.WINDOW.setProperty("accessToken%s" % username, "")
|
|
||||||
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)
|
|
||||||
|
|
||||||
while not self.KodiMonitor.abortRequested():
|
|
||||||
|
|
||||||
# Verify the log level
|
|
||||||
currLogLevel = self.getLogLevel()
|
|
||||||
if self.logLevel != currLogLevel:
|
|
||||||
# Set new log level
|
|
||||||
self.logLevel = 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")
|
|
||||||
|
|
||||||
if status == "restricted":
|
|
||||||
# Parental control is restricting access
|
|
||||||
self.HasAccess = False
|
|
||||||
|
|
||||||
elif status == "401":
|
|
||||||
self.WINDOW.setProperty("Server_status", "Auth")
|
|
||||||
# Revoked token
|
|
||||||
self.resetClient()
|
|
||||||
|
|
||||||
if self.auth and (self.currUser == None):
|
|
||||||
status = self.WINDOW.getProperty("Server_status")
|
|
||||||
|
|
||||||
if (status == "") or (status == "Auth"):
|
|
||||||
self.auth = False
|
|
||||||
self.authenticate()
|
|
||||||
|
|
||||||
if (self.auth == False) and (self.currUser == None):
|
|
||||||
# Only if there's information found to login
|
|
||||||
server = self.getServer()
|
|
||||||
username = self.getUsername()
|
|
||||||
status = self.WINDOW.getProperty("Server_status")
|
|
||||||
|
|
||||||
# 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)
|
|
||||||
self.auth = True
|
|
||||||
|
|
||||||
# If stopping the client didn't work
|
|
||||||
if self.stopClient == True:
|
|
||||||
break
|
|
||||||
|
|
||||||
if self.KodiMonitor.waitForAbort(1):
|
|
||||||
# Abort was requested while waiting. We should exit
|
|
||||||
break
|
|
||||||
|
|
||||||
self.doUtils.stopSession()
|
|
||||||
self.logMsg("|---- UserClient Stopped ----|", 0)
|
|
||||||
|
|
||||||
def stopClient(self):
|
|
||||||
# As last resort
|
|
||||||
self.stopClient = True
|
|
|
@ -1,425 +0,0 @@
|
||||||
#################################################################################################
|
|
||||||
# utils
|
|
||||||
#################################################################################################
|
|
||||||
|
|
||||||
import xbmc
|
|
||||||
import xbmcgui
|
|
||||||
import xbmcaddon
|
|
||||||
import xbmcvfs
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import cProfile
|
|
||||||
import pstats
|
|
||||||
import time
|
|
||||||
import inspect
|
|
||||||
import sqlite3
|
|
||||||
import string
|
|
||||||
import unicodedata
|
|
||||||
import xml.etree.ElementTree as etree
|
|
||||||
|
|
||||||
from API import API
|
|
||||||
from PlayUtils import PlayUtils
|
|
||||||
from DownloadUtils import DownloadUtils
|
|
||||||
|
|
||||||
downloadUtils = DownloadUtils()
|
|
||||||
addon = xbmcaddon.Addon()
|
|
||||||
language = addon.getLocalizedString
|
|
||||||
|
|
||||||
|
|
||||||
def logMsg(title, msg, level = 1):
|
|
||||||
|
|
||||||
WINDOW = xbmcgui.Window(10000)
|
|
||||||
# Get the logLevel set in UserClient
|
|
||||||
logLevel = int(WINDOW.getProperty('getLogLevel'))
|
|
||||||
|
|
||||||
if(logLevel >= level):
|
|
||||||
if(logLevel == 2): # inspect.stack() is expensive
|
|
||||||
try:
|
|
||||||
xbmc.log(title + " -> " + inspect.stack()[1][3] + " : " + str(msg))
|
|
||||||
except UnicodeEncodeError:
|
|
||||||
xbmc.log(title + " -> " + inspect.stack()[1][3] + " : " + str(msg.encode('utf-8')))
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
xbmc.log(title + " -> " + str(msg))
|
|
||||||
except UnicodeEncodeError:
|
|
||||||
xbmc.log(title + " -> " + str(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 KodiSQL(type="video"):
|
|
||||||
|
|
||||||
if type == "music":
|
|
||||||
dbPath = getKodiMusicDBPath()
|
|
||||||
elif type == "texture":
|
|
||||||
dbPath = xbmc.translatePath("special://database/Textures13.db").decode('utf-8')
|
|
||||||
else:
|
|
||||||
dbPath = getKodiVideoDBPath()
|
|
||||||
|
|
||||||
connection = sqlite3.connect(dbPath)
|
|
||||||
return connection
|
|
||||||
|
|
||||||
def getKodiVideoDBPath():
|
|
||||||
|
|
||||||
kodibuild = xbmc.getInfoLabel('System.BuildVersion')[:2]
|
|
||||||
dbVersion = {
|
|
||||||
|
|
||||||
"13": 78, # Gotham
|
|
||||||
"14": 90, # Helix
|
|
||||||
"15": 93, # Isengard
|
|
||||||
"16": 99 # Jarvis
|
|
||||||
}
|
|
||||||
|
|
||||||
dbPath = xbmc.translatePath(
|
|
||||||
"special://database/MyVideos%s.db"
|
|
||||||
% dbVersion.get(kodibuild, "")).decode('utf-8')
|
|
||||||
return dbPath
|
|
||||||
|
|
||||||
def getKodiMusicDBPath():
|
|
||||||
|
|
||||||
kodibuild = xbmc.getInfoLabel('System.BuildVersion')[:2]
|
|
||||||
dbVersion = {
|
|
||||||
|
|
||||||
"13": 46, # Gotham
|
|
||||||
"14": 48, # Helix
|
|
||||||
"15": 52, # Isengard
|
|
||||||
"16": 56 # Jarvis
|
|
||||||
}
|
|
||||||
|
|
||||||
dbPath = xbmc.translatePath(
|
|
||||||
"special://database/MyMusic%s.db"
|
|
||||||
% dbVersion.get(kodibuild, "")).decode('utf-8')
|
|
||||||
return dbPath
|
|
||||||
|
|
||||||
def prettifyXml(elem):
|
|
||||||
rough_string = etree.tostring(elem, "utf-8")
|
|
||||||
reparsed = minidom.parseString(rough_string)
|
|
||||||
return reparsed.toprettyxml(indent="\t")
|
|
||||||
|
|
||||||
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'))
|
|
||||||
|
|
||||||
fileTimeStamp = time.strftime("%Y-%m-%d %H-%M-%S")
|
|
||||||
tabFileNamepath = os.path.join(addondir, "profiles")
|
|
||||||
tabFileName = os.path.join(addondir, "profiles" , profileName + "_profile_(" + fileTimeStamp + ").tab")
|
|
||||||
|
|
||||||
if not xbmcvfs.exists(tabFileNamepath):
|
|
||||||
xbmcvfs.mkdir(tabFileNamepath)
|
|
||||||
|
|
||||||
f = open(tabFileName, 'wb')
|
|
||||||
f.write("NumbCalls\tTotalTime\tCumulativeTime\tFunctionName\tFileName\r\n")
|
|
||||||
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")
|
|
||||||
except ValueError:
|
|
||||||
f.write(str(ncalls) + "\t" + "{0}".format(total_time) + "\t" + "{0}".format(cumulative_time) + "\t" + func_name + "\t" + filename + "\r\n")
|
|
||||||
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(":", "")
|
|
||||||
text = text.replace("/", "-")
|
|
||||||
text = text.replace("\\", "-")
|
|
||||||
text = text.replace("<", "")
|
|
||||||
text = text.replace(">", "")
|
|
||||||
text = text.replace("*", "")
|
|
||||||
text = text.replace("?", "")
|
|
||||||
text = text.replace('|', "")
|
|
||||||
text = text.replace('(', "")
|
|
||||||
text = text.replace(')', "")
|
|
||||||
text = text.strip()
|
|
||||||
# Remove dots from the last character as windows can not have directories
|
|
||||||
# with dots at the end
|
|
||||||
text = text.rstrip('.')
|
|
||||||
text = unicodedata.normalize('NFKD', unicode(text, 'utf-8')).encode('ascii', 'ignore')
|
|
||||||
|
|
||||||
return text
|
|
||||||
|
|
||||||
def reloadProfile():
|
|
||||||
# Useful to reload the add-on without restarting Kodi.
|
|
||||||
profile = xbmc.getInfoLabel('System.ProfileName')
|
|
||||||
xbmc.executebuiltin("LoadProfile(%s)" % profile)
|
|
||||||
|
|
||||||
|
|
||||||
def reset():
|
|
||||||
|
|
||||||
WINDOW = xbmcgui.Window( 10000 )
|
|
||||||
return_value = xbmcgui.Dialog().yesno("Warning", "Are you sure you want to reset your local Kodi database?")
|
|
||||||
|
|
||||||
if return_value == 0:
|
|
||||||
return
|
|
||||||
|
|
||||||
# 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)")
|
|
||||||
|
|
||||||
#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)
|
|
||||||
|
|
||||||
settings('SyncInstallRunDone', "false")
|
|
||||||
|
|
||||||
# Ask if user information should be deleted too.
|
|
||||||
return_user = xbmcgui.Dialog().yesno("Warning", "Reset all Emby Addon settings?")
|
|
||||||
if return_user == 1:
|
|
||||||
WINDOW.setProperty('deletesettings', "true")
|
|
||||||
addon = xbmcaddon.Addon()
|
|
||||||
addondir = xbmc.translatePath(addon.getAddonInfo('profile')).decode('utf-8')
|
|
||||||
dataPath = "%ssettings.xml" % addondir
|
|
||||||
xbmcvfs.delete(dataPath)
|
|
||||||
logMsg("EMBY", "Deleting: settings.xml", 1)
|
|
||||||
|
|
||||||
# first stop any db sync
|
|
||||||
WINDOW.setProperty("SyncDatabaseShouldStop", "true")
|
|
||||||
|
|
||||||
count = 0
|
|
||||||
while(WINDOW.getProperty("SyncDatabaseRunning") == "true"):
|
|
||||||
xbmc.log("Sync Running, will wait : " + str(count))
|
|
||||||
count += 1
|
|
||||||
if(count > 10):
|
|
||||||
dialog = xbmcgui.Dialog()
|
|
||||||
dialog.ok('Warning', 'Could not stop DB sync, you should try again.')
|
|
||||||
return
|
|
||||||
xbmc.sleep(1000)
|
|
||||||
|
|
||||||
# delete video db table data
|
|
||||||
print "Doing Video DB Reset"
|
|
||||||
connection = KodiSQL("video")
|
|
||||||
cursor = connection.cursor( )
|
|
||||||
cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"')
|
|
||||||
rows = cursor.fetchall()
|
|
||||||
for row in rows:
|
|
||||||
tableName = row[0]
|
|
||||||
if(tableName != "version"):
|
|
||||||
cursor.execute("DELETE FROM " + tableName)
|
|
||||||
cursor.execute("DROP TABLE IF EXISTS emby")
|
|
||||||
connection.commit()
|
|
||||||
cursor.close()
|
|
||||||
|
|
||||||
if settings('enableMusicSync') == "true":
|
|
||||||
# delete video db table data
|
|
||||||
print "Doing Music DB Reset"
|
|
||||||
connection = KodiSQL("music")
|
|
||||||
cursor = connection.cursor( )
|
|
||||||
cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"')
|
|
||||||
rows = cursor.fetchall()
|
|
||||||
for row in rows:
|
|
||||||
tableName = row[0]
|
|
||||||
if(tableName != "version"):
|
|
||||||
cursor.execute("DELETE FROM " + tableName)
|
|
||||||
cursor.execute("DROP TABLE IF EXISTS emby")
|
|
||||||
connection.commit()
|
|
||||||
cursor.close()
|
|
||||||
|
|
||||||
|
|
||||||
# reset the install run flag
|
|
||||||
#settings('SyncInstallRunDone', "false")
|
|
||||||
#WINDOW.setProperty("SyncInstallRunDone", "false")
|
|
||||||
|
|
||||||
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")
|
|
|
@ -1,466 +0,0 @@
|
||||||
#################################################################################################
|
|
||||||
# VideoNodes - utils to create video nodes listings in kodi for the emby addon
|
|
||||||
#################################################################################################
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
from ReadEmbyDB import ReadEmbyDB
|
|
||||||
WINDOW = xbmcgui.Window(10000)
|
|
||||||
|
|
||||||
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'))
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
#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)
|
|
||||||
|
|
||||||
#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)
|
|
||||||
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)
|
|
||||||
|
|
||||||
#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)
|
|
||||||
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)
|
|
||||||
try:
|
|
||||||
etree.ElementTree(root).write(nodefile, xml_declaration=True)
|
|
||||||
except:
|
|
||||||
etree.ElementTree(root).write(nodefile)
|
|
||||||
|
|
||||||
#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)
|
|
||||||
try:
|
|
||||||
etree.ElementTree(root).write(nodefile, xml_declaration=True)
|
|
||||||
except:
|
|
||||||
etree.ElementTree(root).write(nodefile)
|
|
||||||
|
|
||||||
#sets
|
|
||||||
nodefile = os.path.join(libraryPath, tagname_normalized + "_sets.xml")
|
|
||||||
root = etree.Element("node", {"order":"9", "type":"filter"})
|
|
||||||
label = xbmc.getLocalizedString(20434)
|
|
||||||
etree.SubElement(root, "label").text = label
|
|
||||||
etree.SubElement(root, "match").text = "all"
|
|
||||||
etree.SubElement(root, "group").text = "sets"
|
|
||||||
etree.SubElement(root, "content").text = type
|
|
||||||
etree.SubElement(root, "icon").text = "special://home/addons/plugin.video.emby/icon.png"
|
|
||||||
etree.SubElement(root, "order", {"direction":"ascending"}).text = "sorttitle"
|
|
||||||
Rule = etree.SubElement(root, "rule", {"field":"tag","operator":"is"})
|
|
||||||
etree.SubElement(Rule, "value").text = tagname
|
|
||||||
WINDOW.setProperty("Emby.nodes.%s.sets.title" %str(windowPropId),label)
|
|
||||||
path = "library://video/Emby - %s/%s_sets.xml"%(tagname_normalized,tagname_normalized)
|
|
||||||
WINDOW.setProperty("Emby.nodes.%s.sets.path" %str(windowPropId),"ActivateWindow(Video,%s,return)"%path)
|
|
||||||
WINDOW.setProperty("Emby.nodes.%s.sets.content" %str(windowPropId),path)
|
|
||||||
try:
|
|
||||||
etree.ElementTree(root).write(nodefile, xml_declaration=True)
|
|
||||||
except:
|
|
||||||
etree.ElementTree(root).write(nodefile)
|
|
||||||
|
|
||||||
#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)
|
|
||||||
|
|
||||||
try:
|
|
||||||
etree.ElementTree(root).write(nodefile, xml_declaration=True)
|
|
||||||
except:
|
|
||||||
etree.ElementTree(root).write(nodefile)
|
|
||||||
|
|
||||||
#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)
|
|
||||||
|
|
||||||
#create tag node - recommended items
|
|
||||||
nodefile = os.path.join(libraryPath, tagname_normalized + "_recommended.xml")
|
|
||||||
root = etree.Element("node", {"order":"10", "type":"filter"})
|
|
||||||
label = language(30230)
|
|
||||||
etree.SubElement(root, "label").text = label
|
|
||||||
etree.SubElement(root, "match").text = "all"
|
|
||||||
etree.SubElement(root, "content").text = type
|
|
||||||
etree.SubElement(root, "icon").text = "special://home/addons/plugin.video.emby/icon.png"
|
|
||||||
Rule = etree.SubElement(root, "rule", {"field":"tag","operator":"is"})
|
|
||||||
etree.SubElement(Rule, "value").text = tagname
|
|
||||||
Rule2 = etree.SubElement(root, "rule", {"field":"playcount","operator":"is"})
|
|
||||||
etree.SubElement(Rule2, "value").text = "0"
|
|
||||||
Rule3 = etree.SubElement(root, "rule", {"field":"rating","operator":"greaterthan"})
|
|
||||||
etree.SubElement(Rule3, "value").text = "7"
|
|
||||||
#set limit to 25 --> currently hardcoded --> TODO: add a setting for this ?
|
|
||||||
etree.SubElement(root, "limit").text = "25"
|
|
||||||
etree.SubElement(root, "order", {"direction":"descending"}).text = "rating"
|
|
||||||
WINDOW.setProperty("Emby.nodes.%s.random.title" %str(windowPropId),label)
|
|
||||||
path = "library://video/Emby - %s/%s_recommended.xml"%(tagname_normalized,tagname_normalized)
|
|
||||||
WINDOW.setProperty("Emby.nodes.%s.recommended.path" %str(windowPropId),"ActivateWindow(Video,%s,return)"%path)
|
|
||||||
WINDOW.setProperty("Emby.nodes.%s.recommended.content" %str(windowPropId),path)
|
|
||||||
try:
|
|
||||||
etree.ElementTree(root).write(nodefile, xml_declaration=True)
|
|
||||||
except:
|
|
||||||
etree.ElementTree(root).write(nodefile)
|
|
||||||
|
|
||||||
#### TAGS ONLY FOR TV SHOWS COLLECTIONS ####
|
|
||||||
if type == "tvshows":
|
|
||||||
|
|
||||||
#as from kodi isengard you can use tags for episodes to filter
|
|
||||||
#for below isengard we still use the plugin's entrypoint to build a listing
|
|
||||||
if kodiVersion == 15:
|
|
||||||
#create tag node - recent episodes
|
|
||||||
nodefile = os.path.join(libraryPath, tagname_normalized + "_recent_episodes.xml")
|
|
||||||
root = etree.Element("node", {"order":"3", "type":"filter"})
|
|
||||||
label = language(30175)
|
|
||||||
etree.SubElement(root, "label").text = label
|
|
||||||
etree.SubElement(root, "match").text = "all"
|
|
||||||
etree.SubElement(root, "content").text = "episodes"
|
|
||||||
etree.SubElement(root, "icon").text = "special://home/addons/plugin.video.emby/icon.png"
|
|
||||||
etree.SubElement(root, "order", {"direction":"descending"}).text = "dateadded"
|
|
||||||
Rule = etree.SubElement(root, "rule", {"field":"tag","operator":"is"})
|
|
||||||
etree.SubElement(Rule, "value").text = tagname
|
|
||||||
#set limit to 25 --> currently hardcoded --> TODO: add a setting for this ?
|
|
||||||
etree.SubElement(root, "limit").text = "25"
|
|
||||||
#exclude watched items --> currently hardcoded --> TODO: add a setting for this ?
|
|
||||||
Rule2 = etree.SubElement(root, "rule", {"field":"playcount","operator":"is"})
|
|
||||||
etree.SubElement(Rule2, "value").text = "0"
|
|
||||||
WINDOW.setProperty("Emby.nodes.%s.recentepisodes.title" %str(windowPropId),label)
|
|
||||||
path = "library://video/Emby - %s/%s_recent_episodes.xml"%(tagname_normalized,tagname_normalized)
|
|
||||||
WINDOW.setProperty("Emby.nodes.%s.recentepisodes.path" %str(windowPropId),"ActivateWindow(Video,%s,return)"%path)
|
|
||||||
WINDOW.setProperty("Emby.nodes.%s.recentepisodes.content" %str(windowPropId),path)
|
|
||||||
try:
|
|
||||||
etree.ElementTree(root).write(nodefile, xml_declaration=True)
|
|
||||||
except:
|
|
||||||
etree.ElementTree(root).write(nodefile)
|
|
||||||
|
|
||||||
#create tag node - inprogress episodes
|
|
||||||
nodefile = os.path.join(libraryPath, tagname_normalized + "_progress_episodes.xml")
|
|
||||||
root = etree.Element("node", {"order":"4", "type":"filter"})
|
|
||||||
label = language(30178)
|
|
||||||
etree.SubElement(root, "label").text = label
|
|
||||||
etree.SubElement(root, "match").text = "all"
|
|
||||||
etree.SubElement(root, "content").text = "episodes"
|
|
||||||
etree.SubElement(root, "icon").text = "special://home/addons/plugin.video.emby/icon.png"
|
|
||||||
Rule = etree.SubElement(root, "rule", {"field":"tag","operator":"is"})
|
|
||||||
etree.SubElement(Rule, "value").text = tagname
|
|
||||||
#set limit to 25 --> currently hardcoded --> TODO: add a setting for this ?
|
|
||||||
etree.SubElement(root, "limit").text = "25"
|
|
||||||
Rule2 = etree.SubElement(root, "rule", {"field":"inprogress","operator":"true"})
|
|
||||||
WINDOW.setProperty("Emby.nodes.%s.inprogressepisodes.title" %str(windowPropId),label)
|
|
||||||
path = "library://video/Emby - %s/%s_progress_episodes.xml"%(tagname_normalized,tagname_normalized)
|
|
||||||
WINDOW.setProperty("Emby.nodes.%s.inprogressepisodes.path" %str(windowPropId),"ActivateWindow(Video,%s,return)"%path)
|
|
||||||
WINDOW.setProperty("Emby.nodes.%s.inprogressepisodes.content" %str(windowPropId),path)
|
|
||||||
try:
|
|
||||||
etree.ElementTree(root).write(nodefile, xml_declaration=True)
|
|
||||||
except:
|
|
||||||
etree.ElementTree(root).write(nodefile)
|
|
||||||
|
|
||||||
if kodiVersion == 14:
|
|
||||||
#create tag node - recent episodes
|
|
||||||
nodefile = os.path.join(libraryPath, tagname_normalized + "_recent_episodes.xml")
|
|
||||||
root = etree.Element("node", {"order":"4", "type":"folder"})
|
|
||||||
label = language(30175)
|
|
||||||
etree.SubElement(root, "label").text = label
|
|
||||||
etree.SubElement(root, "content").text = "episodes"
|
|
||||||
etree.SubElement(root, "icon").text = "special://home/addons/plugin.video.emby/icon.png"
|
|
||||||
path = "plugin://plugin.video.emby/?id=%s&mode=recentepisodes&limit=25" %tagname
|
|
||||||
etree.SubElement(root, "path").text = path
|
|
||||||
WINDOW.setProperty("Emby.nodes.%s.recentepisodes.title" %str(windowPropId),label)
|
|
||||||
path = "library://video/Emby - %s/%s_recent_episodes.xml"%(tagname_normalized,tagname_normalized)
|
|
||||||
WINDOW.setProperty("Emby.nodes.%s.recentepisodes.path" %str(windowPropId),"ActivateWindow(Video,%s,return)"%path)
|
|
||||||
WINDOW.setProperty("Emby.nodes.%s.recentepisodes.content" %str(windowPropId),path)
|
|
||||||
try:
|
|
||||||
etree.ElementTree(root).write(nodefile, xml_declaration=True)
|
|
||||||
except:
|
|
||||||
etree.ElementTree(root).write(nodefile)
|
|
||||||
|
|
||||||
#create tag node - inprogress items
|
|
||||||
nodefile = os.path.join(libraryPath, tagname_normalized + "_progress_episodes.xml")
|
|
||||||
root = etree.Element("node", {"order":"5", "type":"folder"})
|
|
||||||
label = language(30178)
|
|
||||||
etree.SubElement(root, "label").text = label
|
|
||||||
etree.SubElement(root, "content").text = "episodes"
|
|
||||||
etree.SubElement(root, "icon").text = "special://home/addons/plugin.video.emby/icon.png"
|
|
||||||
path = "plugin://plugin.video.emby/?id=%s&mode=inprogressepisodes&limit=25" %tagname
|
|
||||||
etree.SubElement(root, "path").text = path
|
|
||||||
WINDOW.setProperty("Emby.nodes.%s.inprogressepisodes.title" %str(windowPropId),label)
|
|
||||||
path = "library://video/Emby - %s/%s_progress_episodes.xml"%(tagname_normalized,tagname_normalized)
|
|
||||||
WINDOW.setProperty("Emby.nodes.%s.inprogressepisodes.path" %str(windowPropId),"ActivateWindow(Video,%s,return)"%path)
|
|
||||||
WINDOW.setProperty("Emby.nodes.%s.inprogressepisodes.content" %str(windowPropId),path)
|
|
||||||
try:
|
|
||||||
etree.ElementTree(root).write(nodefile, xml_declaration=True)
|
|
||||||
except:
|
|
||||||
etree.ElementTree(root).write(nodefile)
|
|
||||||
|
|
||||||
#create tag node - nextup items
|
|
||||||
#for nextup we always use the dynamic content approach with the plugin's entrypoint because it involves a custom query
|
|
||||||
nodefile = os.path.join(libraryPath, tagname_normalized + "_nextup_episodes.xml")
|
|
||||||
root = etree.Element("node", {"order":"6", "type":"folder"})
|
|
||||||
label = language(30179)
|
|
||||||
etree.SubElement(root, "label").text = label
|
|
||||||
etree.SubElement(root, "content").text = "episodes"
|
|
||||||
path = "plugin://plugin.video.emby/?id=%s&mode=nextup&limit=25" %tagname
|
|
||||||
etree.SubElement(root, "path").text = path
|
|
||||||
etree.SubElement(root, "icon").text = "special://home/addons/plugin.video.emby/icon.png"
|
|
||||||
WINDOW.setProperty("Emby.nodes.%s.nextepisodes.title" %str(windowPropId),label)
|
|
||||||
path = "library://video/Emby - %s/%s_nextup_episodes.xml"%(tagname_normalized,tagname_normalized)
|
|
||||||
WINDOW.setProperty("Emby.nodes.%s.nextepisodes.path" %str(windowPropId),"ActivateWindow(Video,%s,return)"%path)
|
|
||||||
WINDOW.setProperty("Emby.nodes.%s.nextepisodes.content" %str(windowPropId),path)
|
|
||||||
try:
|
|
||||||
etree.ElementTree(root).write(nodefile, xml_declaration=True)
|
|
||||||
except:
|
|
||||||
etree.ElementTree(root).write(nodefile)
|
|
||||||
|
|
||||||
def buildVideoNodesListing(self):
|
|
||||||
|
|
||||||
try:
|
|
||||||
|
|
||||||
# the library path doesn't exist on all systems
|
|
||||||
if not xbmcvfs.exists("special://profile/library/"):
|
|
||||||
xbmcvfs.mkdir("special://profile/library")
|
|
||||||
if not xbmcvfs.exists("special://profile/library/video/"):
|
|
||||||
#we need to copy over the default items
|
|
||||||
shutil.copytree(xbmc.translatePath("special://xbmc/system/library/video"), xbmc.translatePath("special://profile/library/video"))
|
|
||||||
|
|
||||||
#always cleanup existing Emby video nodes first because we don't want old stuff to stay in there
|
|
||||||
path = "special://profile/library/video/"
|
|
||||||
if xbmcvfs.exists(path):
|
|
||||||
allDirs, allFiles = xbmcvfs.listdir(path)
|
|
||||||
for dir in allDirs:
|
|
||||||
if dir.startswith("Emby "):
|
|
||||||
shutil.rmtree(xbmc.translatePath("special://profile/library/video/" + dir))
|
|
||||||
for file in allFiles:
|
|
||||||
if file.startswith("emby"):
|
|
||||||
xbmcvfs.delete(path + file)
|
|
||||||
|
|
||||||
#we build up a listing and set window props for all nodes we created
|
|
||||||
#the window props will be used by the main entry point to quickly build up the listing and can be used in skins (like titan) too for quick reference
|
|
||||||
#comment marcelveldt: please leave the window props as-is because I will be referencing them in titan skin...
|
|
||||||
totalNodesCount = 0
|
|
||||||
|
|
||||||
#build the listing for all views
|
|
||||||
views_movies = ReadEmbyDB().getCollections("movies")
|
|
||||||
if views_movies:
|
|
||||||
for view in views_movies:
|
|
||||||
title = view.get('title')
|
|
||||||
content = view.get('content')
|
|
||||||
if content == "mixed":
|
|
||||||
title = "%s - Movies" % title
|
|
||||||
self.buildVideoNodeForView(title, "movies", totalNodesCount)
|
|
||||||
totalNodesCount +=1
|
|
||||||
|
|
||||||
views_shows = ReadEmbyDB().getCollections("tvshows")
|
|
||||||
if views_shows:
|
|
||||||
for view in views_shows:
|
|
||||||
title = view.get('title')
|
|
||||||
content = view.get('content')
|
|
||||||
if content == "mixed":
|
|
||||||
title = "%s - TV Shows" % title
|
|
||||||
self.buildVideoNodeForView(title, "tvshows", totalNodesCount)
|
|
||||||
totalNodesCount +=1
|
|
||||||
|
|
||||||
#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
|
|
Loading…
Reference in a new issue