Readd with lowercase name
This commit is contained in:
parent
9855ac4c94
commit
69884a1b54
11 changed files with 5708 additions and 0 deletions
378
resources/lib/api.py
Normal file
378
resources/lib/api.py
Normal file
|
@ -0,0 +1,378 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
##################################################################################################
|
||||||
|
|
||||||
|
import clientinfo
|
||||||
|
import utils
|
||||||
|
|
||||||
|
##################################################################################################
|
||||||
|
|
||||||
|
|
||||||
|
class API():
|
||||||
|
|
||||||
|
def __init__(self, item):
|
||||||
|
|
||||||
|
self.item = item
|
||||||
|
self.clientinfo = clientinfo.ClientInfo()
|
||||||
|
self.addonName = self.clientinfo.getAddonName()
|
||||||
|
|
||||||
|
def logMsg(self, msg, lvl=1):
|
||||||
|
|
||||||
|
className = self.__class__.__name__
|
||||||
|
utils.logMsg("%s %s" % (self.addonName, className), msg, lvl)
|
||||||
|
|
||||||
|
|
||||||
|
def getUserData(self):
|
||||||
|
# Default
|
||||||
|
favorite = False
|
||||||
|
playcount = None
|
||||||
|
played = False
|
||||||
|
lastPlayedDate = None
|
||||||
|
resume = 0
|
||||||
|
rating = 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
userdata = self.item['UserData']
|
||||||
|
|
||||||
|
except KeyError: # No userdata found.
|
||||||
|
pass
|
||||||
|
|
||||||
|
else:
|
||||||
|
favorite = userdata['IsFavorite']
|
||||||
|
likes = userdata.get('Likes')
|
||||||
|
# Rating for album and songs
|
||||||
|
if favorite:
|
||||||
|
rating = 5
|
||||||
|
elif likes:
|
||||||
|
rating = 3
|
||||||
|
elif likes == False:
|
||||||
|
rating = 1
|
||||||
|
else:
|
||||||
|
rating = 0
|
||||||
|
|
||||||
|
lastPlayedDate = userdata.get('LastPlayedDate')
|
||||||
|
if lastPlayedDate:
|
||||||
|
lastPlayedDate = lastPlayedDate.split('.')[0].replace('T', " ")
|
||||||
|
|
||||||
|
if userdata['Played']:
|
||||||
|
# Playcount is tied to the watch status
|
||||||
|
played = True
|
||||||
|
playcount = userdata['PlayCount']
|
||||||
|
if playcount == 0:
|
||||||
|
playcount = 1
|
||||||
|
|
||||||
|
if lastPlayedDate is None:
|
||||||
|
lastPlayedDate = self.getDateCreated()
|
||||||
|
|
||||||
|
playbackPosition = userdata.get('PlaybackPositionTicks')
|
||||||
|
if playbackPosition:
|
||||||
|
resume = playbackPosition / 10000000.0
|
||||||
|
|
||||||
|
return {
|
||||||
|
|
||||||
|
'Favorite': favorite,
|
||||||
|
'PlayCount': playcount,
|
||||||
|
'Played': played,
|
||||||
|
'LastPlayedDate': lastPlayedDate,
|
||||||
|
'Resume': resume,
|
||||||
|
'Rating': rating
|
||||||
|
}
|
||||||
|
|
||||||
|
def getPeople(self):
|
||||||
|
# Process People
|
||||||
|
director = []
|
||||||
|
writer = []
|
||||||
|
cast = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
people = self.item['People']
|
||||||
|
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
else:
|
||||||
|
for person in people:
|
||||||
|
|
||||||
|
type = person['Type']
|
||||||
|
name = person['Name']
|
||||||
|
|
||||||
|
if "Director" in type:
|
||||||
|
director.append(name)
|
||||||
|
elif "Actor" in type:
|
||||||
|
cast.append(name)
|
||||||
|
elif type in ("Writing", "Writer"):
|
||||||
|
writer.append(name)
|
||||||
|
|
||||||
|
return {
|
||||||
|
|
||||||
|
'Director': director,
|
||||||
|
'Writer': writer,
|
||||||
|
'Cast': cast
|
||||||
|
}
|
||||||
|
|
||||||
|
def getMediaStreams(self):
|
||||||
|
item = self.item
|
||||||
|
videotracks = []
|
||||||
|
audiotracks = []
|
||||||
|
subtitlelanguages = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
media_streams = item['MediaSources'][0]['MediaStreams']
|
||||||
|
|
||||||
|
except KeyError:
|
||||||
|
media_streams = item['MediaStreams']
|
||||||
|
|
||||||
|
for media_stream in media_streams:
|
||||||
|
# Sort through Video, Audio, Subtitle
|
||||||
|
stream_type = media_stream['Type']
|
||||||
|
codec = media_stream.get('Codec', "").lower()
|
||||||
|
profile = media_stream.get('Profile', "").lower()
|
||||||
|
|
||||||
|
if stream_type == "Video":
|
||||||
|
# Height, Width, Codec, AspectRatio, AspectFloat, 3D
|
||||||
|
track = {
|
||||||
|
|
||||||
|
'videocodec': codec,
|
||||||
|
'height': media_stream.get('Height'),
|
||||||
|
'width': media_stream.get('Width'),
|
||||||
|
'video3DFormat': item.get('Video3DFormat'),
|
||||||
|
'aspectratio': 1.85
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
container = item['MediaSources'][0]['Container'].lower()
|
||||||
|
except:
|
||||||
|
container = ""
|
||||||
|
|
||||||
|
# Sort codec vs container/profile
|
||||||
|
if "msmpeg4" in codec:
|
||||||
|
track['videocodec'] = "divx"
|
||||||
|
elif "mpeg4" in codec:
|
||||||
|
if "simple profile" in profile or not profile:
|
||||||
|
track['videocodec'] = "xvid"
|
||||||
|
elif "h264" in codec:
|
||||||
|
if container in ("mp4", "mov", "m4v"):
|
||||||
|
track['videocodec'] = "avc1"
|
||||||
|
|
||||||
|
# Aspect ratio
|
||||||
|
if item.get('AspectRatio'):
|
||||||
|
# Metadata AR
|
||||||
|
aspectratio = item['AspectRatio']
|
||||||
|
else: # File AR
|
||||||
|
aspectratio = media_stream.get('AspectRatio', "0")
|
||||||
|
|
||||||
|
try:
|
||||||
|
aspectwidth, aspectheight = aspectratio.split(':')
|
||||||
|
track['aspectratio'] = round(float(aspectwidth) / float(aspectheight), 6)
|
||||||
|
|
||||||
|
except ValueError:
|
||||||
|
width = track['width']
|
||||||
|
height = track['height']
|
||||||
|
|
||||||
|
if width and height:
|
||||||
|
track['aspectratio'] = round(float(width / height), 6)
|
||||||
|
|
||||||
|
videotracks.append(track)
|
||||||
|
|
||||||
|
elif stream_type == "Audio":
|
||||||
|
# Codec, Channels, language
|
||||||
|
track = {
|
||||||
|
|
||||||
|
'audiocodec': codec,
|
||||||
|
'channels': media_stream.get('Channels'),
|
||||||
|
'audiolanguage': media_stream.get('Language')
|
||||||
|
}
|
||||||
|
|
||||||
|
if "dca" in codec and "dts-hd ma" in profile:
|
||||||
|
track['audiocodec'] = "dtshd_ma"
|
||||||
|
|
||||||
|
audiotracks.append(track)
|
||||||
|
|
||||||
|
elif stream_type == "Subtitle":
|
||||||
|
# Language
|
||||||
|
subtitlelanguages.append(media_stream.get('Language', "Unknown"))
|
||||||
|
|
||||||
|
return {
|
||||||
|
|
||||||
|
'video': videotracks,
|
||||||
|
'audio': audiotracks,
|
||||||
|
'subtitle': subtitlelanguages
|
||||||
|
}
|
||||||
|
|
||||||
|
def getRuntime(self):
|
||||||
|
item = self.item
|
||||||
|
try:
|
||||||
|
runtime = item['RunTimeTicks'] / 10000000.0
|
||||||
|
|
||||||
|
except KeyError:
|
||||||
|
runtime = item.get('CumulativeRunTimeTicks', 0) / 10000000.0
|
||||||
|
|
||||||
|
return runtime
|
||||||
|
|
||||||
|
def adjustResume(self, resume_seconds):
|
||||||
|
|
||||||
|
resume = 0
|
||||||
|
if resume_seconds:
|
||||||
|
resume = round(float(resume_seconds), 6)
|
||||||
|
jumpback = int(utils.settings('resumeJumpBack'))
|
||||||
|
if resume > jumpback:
|
||||||
|
# To avoid negative bookmark
|
||||||
|
resume = resume - jumpback
|
||||||
|
|
||||||
|
return resume
|
||||||
|
|
||||||
|
def getStudios(self):
|
||||||
|
# Process Studios
|
||||||
|
item = self.item
|
||||||
|
studios = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
studio = item['SeriesStudio']
|
||||||
|
studios.append(self.verifyStudio(studio))
|
||||||
|
|
||||||
|
except KeyError:
|
||||||
|
studioList = item['Studios']
|
||||||
|
for studio in studioList:
|
||||||
|
|
||||||
|
name = studio['Name']
|
||||||
|
studios.append(self.verifyStudio(name))
|
||||||
|
|
||||||
|
return studios
|
||||||
|
|
||||||
|
def verifyStudio(self, studioName):
|
||||||
|
# Convert studio for Kodi to properly detect them
|
||||||
|
studios = {
|
||||||
|
|
||||||
|
'abc (us)': "ABC",
|
||||||
|
'fox (us)': "FOX",
|
||||||
|
'mtv (us)': "MTV",
|
||||||
|
'showcase (ca)': "Showcase",
|
||||||
|
'wgn america': "WGN"
|
||||||
|
}
|
||||||
|
|
||||||
|
return studios.get(studioName.lower(), studioName)
|
||||||
|
|
||||||
|
def getChecksum(self):
|
||||||
|
# Use the etags checksum and userdata
|
||||||
|
item = self.item
|
||||||
|
userdata = item['UserData']
|
||||||
|
|
||||||
|
checksum = "%s%s%s%s%s%s" % (
|
||||||
|
|
||||||
|
item['Etag'],
|
||||||
|
userdata['Played'],
|
||||||
|
userdata['IsFavorite'],
|
||||||
|
userdata['PlaybackPositionTicks'],
|
||||||
|
userdata.get('UnplayedItemCount', ""),
|
||||||
|
userdata.get('LastPlayedDate', "")
|
||||||
|
)
|
||||||
|
|
||||||
|
return checksum
|
||||||
|
|
||||||
|
def getGenres(self):
|
||||||
|
item = self.item
|
||||||
|
all_genres = ""
|
||||||
|
genres = item.get('Genres', item.get('SeriesGenres'))
|
||||||
|
|
||||||
|
if genres:
|
||||||
|
all_genres = " / ".join(genres)
|
||||||
|
|
||||||
|
return all_genres
|
||||||
|
|
||||||
|
def getDateCreated(self):
|
||||||
|
|
||||||
|
try:
|
||||||
|
dateadded = self.item['DateCreated']
|
||||||
|
dateadded = dateadded.split('.')[0].replace('T', " ")
|
||||||
|
except KeyError:
|
||||||
|
dateadded = None
|
||||||
|
|
||||||
|
return dateadded
|
||||||
|
|
||||||
|
def getPremiereDate(self):
|
||||||
|
|
||||||
|
try:
|
||||||
|
premiere = self.item['PremiereDate']
|
||||||
|
premiere = premiere.split('.')[0].replace('T', " ")
|
||||||
|
except KeyError:
|
||||||
|
premiere = None
|
||||||
|
|
||||||
|
return premiere
|
||||||
|
|
||||||
|
def getOverview(self):
|
||||||
|
|
||||||
|
try:
|
||||||
|
overview = self.item['Overview']
|
||||||
|
overview = overview.replace("\"", "\'")
|
||||||
|
overview = overview.replace("\n", " ")
|
||||||
|
overview = overview.replace("\r", " ")
|
||||||
|
except KeyError:
|
||||||
|
overview = ""
|
||||||
|
|
||||||
|
return overview
|
||||||
|
|
||||||
|
def getTagline(self):
|
||||||
|
|
||||||
|
try:
|
||||||
|
tagline = self.item['Taglines'][0]
|
||||||
|
except IndexError:
|
||||||
|
tagline = None
|
||||||
|
|
||||||
|
return tagline
|
||||||
|
|
||||||
|
def getProvider(self, providername):
|
||||||
|
|
||||||
|
try:
|
||||||
|
provider = self.item['ProviderIds'][providername]
|
||||||
|
except KeyError:
|
||||||
|
provider = None
|
||||||
|
|
||||||
|
return provider
|
||||||
|
|
||||||
|
def getMpaa(self):
|
||||||
|
# Convert more complex cases
|
||||||
|
mpaa = self.item.get('OfficialRating', "")
|
||||||
|
|
||||||
|
if mpaa in ("NR", "UR"):
|
||||||
|
# Kodi seems to not like NR, but will accept Not Rated
|
||||||
|
mpaa = "Not Rated"
|
||||||
|
|
||||||
|
return mpaa
|
||||||
|
|
||||||
|
def getCountry(self):
|
||||||
|
|
||||||
|
try:
|
||||||
|
country = self.item['ProductionLocations'][0]
|
||||||
|
except IndexError:
|
||||||
|
country = None
|
||||||
|
|
||||||
|
return country
|
||||||
|
|
||||||
|
def getFilePath(self):
|
||||||
|
|
||||||
|
item = self.item
|
||||||
|
try:
|
||||||
|
filepath = item['Path']
|
||||||
|
|
||||||
|
except KeyError:
|
||||||
|
filepath = ""
|
||||||
|
|
||||||
|
else:
|
||||||
|
if "\\\\" in filepath:
|
||||||
|
# append smb protocol
|
||||||
|
filepath = filepath.replace("\\\\", "smb://")
|
||||||
|
filepath = filepath.replace("\\", "/")
|
||||||
|
|
||||||
|
if item.get('VideoType'):
|
||||||
|
videotype = item['VideoType']
|
||||||
|
# Specific format modification
|
||||||
|
if 'Dvd'in videotype:
|
||||||
|
filepath = "%s/VIDEO_TS/VIDEO_TS.IFO" % filepath
|
||||||
|
elif 'Bluray' in videotype:
|
||||||
|
filepath = "%s/BDMV/index.bdmv" % filepath
|
||||||
|
|
||||||
|
if "\\" in filepath:
|
||||||
|
# Local path scenario, with special videotype
|
||||||
|
filepath = filepath.replace("/", "\\")
|
||||||
|
|
||||||
|
return filepath
|
401
resources/lib/downloadutils.py
Normal file
401
resources/lib/downloadutils.py
Normal file
|
@ -0,0 +1,401 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
##################################################################################################
|
||||||
|
|
||||||
|
import json
|
||||||
|
import requests
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import xbmc
|
||||||
|
import xbmcgui
|
||||||
|
|
||||||
|
import utils
|
||||||
|
import clientinfo
|
||||||
|
|
||||||
|
##################################################################################################
|
||||||
|
|
||||||
|
# Disable requests logging
|
||||||
|
from requests.packages.urllib3.exceptions import InsecureRequestWarning
|
||||||
|
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
|
||||||
|
#logging.getLogger('requests').setLevel(logging.WARNING)
|
||||||
|
|
||||||
|
##################################################################################################
|
||||||
|
|
||||||
|
|
||||||
|
class DownloadUtils():
|
||||||
|
|
||||||
|
# Borg - multiple instances, shared state
|
||||||
|
_shared_state = {}
|
||||||
|
clientInfo = clientinfo.ClientInfo()
|
||||||
|
addonName = clientInfo.getAddonName()
|
||||||
|
|
||||||
|
# Requests session
|
||||||
|
s = None
|
||||||
|
timeout = 30
|
||||||
|
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
|
||||||
|
self.__dict__ = self._shared_state
|
||||||
|
|
||||||
|
def logMsg(self, msg, lvl=1):
|
||||||
|
|
||||||
|
className = self.__class__.__name__
|
||||||
|
utils.logMsg("%s %s" % (self.addonName, className), msg, lvl)
|
||||||
|
|
||||||
|
|
||||||
|
def setUsername(self, username):
|
||||||
|
# Reserved for userclient only
|
||||||
|
self.username = username
|
||||||
|
self.logMsg("Set username: %s" % username, 2)
|
||||||
|
|
||||||
|
def setUserId(self, userId):
|
||||||
|
# Reserved for userclient only
|
||||||
|
self.userId = userId
|
||||||
|
self.logMsg("Set userId: %s" % userId, 2)
|
||||||
|
|
||||||
|
def setServer(self, server):
|
||||||
|
# Reserved for userclient only
|
||||||
|
self.server = server
|
||||||
|
self.logMsg("Set server: %s" % server, 2)
|
||||||
|
|
||||||
|
def setToken(self, token):
|
||||||
|
# Reserved for userclient only
|
||||||
|
self.token = token
|
||||||
|
self.logMsg("Set token: %s" % token, 2)
|
||||||
|
|
||||||
|
def setSSL(self, ssl, sslclient):
|
||||||
|
# Reserved for userclient only
|
||||||
|
self.sslverify = ssl
|
||||||
|
self.sslclient = sslclient
|
||||||
|
self.logMsg("Verify SSL host certificate: %s" % ssl, 2)
|
||||||
|
self.logMsg("SSL client side certificate: %s" % sslclient, 2)
|
||||||
|
|
||||||
|
|
||||||
|
def postCapabilities(self, deviceId):
|
||||||
|
|
||||||
|
# Post settings to session
|
||||||
|
url = "{server}/emby/Sessions/Capabilities/Full?format=json"
|
||||||
|
data = {
|
||||||
|
|
||||||
|
'PlayableMediaTypes': "Audio,Video",
|
||||||
|
'SupportsMediaControl': True,
|
||||||
|
'SupportedCommands': (
|
||||||
|
|
||||||
|
"MoveUp,MoveDown,MoveLeft,MoveRight,Select,"
|
||||||
|
"Back,ToggleContextMenu,ToggleFullscreen,ToggleOsdMenu,"
|
||||||
|
"GoHome,PageUp,NextLetter,GoToSearch,"
|
||||||
|
"GoToSettings,PageDown,PreviousLetter,TakeScreenshot,"
|
||||||
|
"VolumeUp,VolumeDown,ToggleMute,SendString,DisplayMessage,"
|
||||||
|
"SetAudioStreamIndex,SetSubtitleStreamIndex,"
|
||||||
|
|
||||||
|
"Mute,Unmute,SetVolume,"
|
||||||
|
"Play,Playstate,PlayNext"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.logMsg("Capabilities URL: %s" % url, 2)
|
||||||
|
self.logMsg("Postdata: %s" % data, 2)
|
||||||
|
|
||||||
|
self.downloadUrl(url, postBody=data, type="POST")
|
||||||
|
self.logMsg("Posted capabilities to %s" % self.server, 2)
|
||||||
|
|
||||||
|
# Attempt at getting sessionId
|
||||||
|
url = "{server}/emby/Sessions?DeviceId=%s&format=json" % deviceId
|
||||||
|
result = self.downloadUrl(url)
|
||||||
|
try:
|
||||||
|
sessionId = result[0]['Id']
|
||||||
|
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
self.logMsg("Failed to retrieve sessionId.", 1)
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.logMsg("Session: %s" % result, 2)
|
||||||
|
self.logMsg("SessionId: %s" % sessionId, 1)
|
||||||
|
utils.window('emby_sessionId', value=sessionId)
|
||||||
|
|
||||||
|
# Post any permanent additional users
|
||||||
|
additionalUsers = utils.settings('additionalUsers')
|
||||||
|
if additionalUsers:
|
||||||
|
|
||||||
|
additionalUsers = additionalUsers.split(',')
|
||||||
|
self.logMsg(
|
||||||
|
"List of permanent users added to the session: %s"
|
||||||
|
% additionalUsers, 1)
|
||||||
|
|
||||||
|
# Get the user list from server to get the userId
|
||||||
|
url = "{server}/emby/Users?format=json"
|
||||||
|
result = self.downloadUrl(url)
|
||||||
|
|
||||||
|
for additional in additionalUsers:
|
||||||
|
addUser = additional.decode('utf-8').lower()
|
||||||
|
|
||||||
|
# Compare to server users to list of permanent additional users
|
||||||
|
for user in result:
|
||||||
|
username = user['Name'].lower()
|
||||||
|
|
||||||
|
if username in addUser:
|
||||||
|
userId = user['Id']
|
||||||
|
url = (
|
||||||
|
"{server}/emby/Sessions/%s/Users/%s?format=json"
|
||||||
|
% (sessionId, userId)
|
||||||
|
)
|
||||||
|
self.downloadUrl(url, postBody={}, type="POST")
|
||||||
|
|
||||||
|
|
||||||
|
def startSession(self):
|
||||||
|
|
||||||
|
self.deviceId = self.clientInfo.getDeviceId()
|
||||||
|
|
||||||
|
# User is identified from this point
|
||||||
|
# Attach authenticated header to the session
|
||||||
|
verify = None
|
||||||
|
cert = None
|
||||||
|
header = self.getHeader()
|
||||||
|
|
||||||
|
# If user enabled host certificate verification
|
||||||
|
try:
|
||||||
|
verify = self.sslverify
|
||||||
|
cert = self.sslclient
|
||||||
|
except:
|
||||||
|
self.logMsg("Could not load SSL settings.", 1)
|
||||||
|
|
||||||
|
# Start session
|
||||||
|
self.s = requests.Session()
|
||||||
|
self.s.headers = header
|
||||||
|
self.s.verify = verify
|
||||||
|
self.s.cert = cert
|
||||||
|
# Retry connections to the server
|
||||||
|
self.s.mount("http://", requests.adapters.HTTPAdapter(max_retries=1))
|
||||||
|
self.s.mount("https://", requests.adapters.HTTPAdapter(max_retries=1))
|
||||||
|
|
||||||
|
self.logMsg("Requests session started on: %s" % self.server, 1)
|
||||||
|
|
||||||
|
def stopSession(self):
|
||||||
|
try:
|
||||||
|
self.s.close()
|
||||||
|
except:
|
||||||
|
self.logMsg("Requests session could not be terminated.", 1)
|
||||||
|
|
||||||
|
def getHeader(self, authenticate=True):
|
||||||
|
|
||||||
|
clientInfo = self.clientInfo
|
||||||
|
|
||||||
|
deviceName = clientInfo.getDeviceName()
|
||||||
|
deviceId = clientInfo.getDeviceId()
|
||||||
|
version = clientInfo.getVersion()
|
||||||
|
|
||||||
|
if not authenticate:
|
||||||
|
# If user is not authenticated
|
||||||
|
auth = (
|
||||||
|
'MediaBrowser Client="Kodi", Device="%s", DeviceId="%s", Version="%s"'
|
||||||
|
% (deviceName, deviceId, version))
|
||||||
|
header = {
|
||||||
|
|
||||||
|
'Content-type': 'application/json',
|
||||||
|
'Accept-encoding': 'gzip',
|
||||||
|
'Accept-Charset': 'UTF-8,*',
|
||||||
|
'Authorization': auth
|
||||||
|
}
|
||||||
|
self.logMsg("Header: %s" % header, 2)
|
||||||
|
|
||||||
|
else:
|
||||||
|
userId = self.userId
|
||||||
|
token = self.token
|
||||||
|
# Attached to the requests session
|
||||||
|
auth = (
|
||||||
|
'MediaBrowser UserId="%s", Client="Kodi", Device="%s", DeviceId="%s", Version="%s"'
|
||||||
|
% (userId, deviceName, deviceId, version))
|
||||||
|
header = {
|
||||||
|
|
||||||
|
'Content-type': 'application/json',
|
||||||
|
'Accept-encoding': 'gzip',
|
||||||
|
'Accept-Charset': 'UTF-8,*',
|
||||||
|
'Authorization': auth,
|
||||||
|
'X-MediaBrowser-Token': token
|
||||||
|
}
|
||||||
|
self.logMsg("Header: %s" % header, 2)
|
||||||
|
|
||||||
|
return header
|
||||||
|
|
||||||
|
def downloadUrl(self, url, postBody=None, type="GET", parameters=None, authenticate=True):
|
||||||
|
|
||||||
|
self.logMsg("=== ENTER downloadUrl ===", 2)
|
||||||
|
|
||||||
|
timeout = self.timeout
|
||||||
|
default_link = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
# If user is authenticated
|
||||||
|
if (authenticate):
|
||||||
|
# Get requests session
|
||||||
|
try:
|
||||||
|
s = self.s
|
||||||
|
# Replace for the real values
|
||||||
|
url = url.replace("{server}", self.server)
|
||||||
|
url = url.replace("{UserId}", self.userId)
|
||||||
|
|
||||||
|
# Prepare request
|
||||||
|
if type == "GET":
|
||||||
|
r = s.get(url, json=postBody, params=parameters, timeout=timeout)
|
||||||
|
elif type == "POST":
|
||||||
|
r = s.post(url, json=postBody, timeout=timeout)
|
||||||
|
elif type == "DELETE":
|
||||||
|
r = s.delete(url, json=postBody, timeout=timeout)
|
||||||
|
|
||||||
|
except AttributeError:
|
||||||
|
# request session does not exists
|
||||||
|
# Get user information
|
||||||
|
self.userId = utils.window('emby_currUser')
|
||||||
|
self.server = utils.window('emby_server%s' % self.userId)
|
||||||
|
self.token = utils.window('emby_accessToken%s' % self.userId)
|
||||||
|
header = self.getHeader()
|
||||||
|
verifyssl = False
|
||||||
|
cert = None
|
||||||
|
|
||||||
|
# IF user enables ssl verification
|
||||||
|
if utils.settings('sslverify') == "true":
|
||||||
|
verifyssl = True
|
||||||
|
if utils.settings('sslcert') != "None":
|
||||||
|
cert = utils.settings('sslcert')
|
||||||
|
|
||||||
|
# Replace for the real values
|
||||||
|
url = url.replace("{server}", self.server)
|
||||||
|
url = url.replace("{UserId}", self.userId)
|
||||||
|
|
||||||
|
# Prepare request
|
||||||
|
if type == "GET":
|
||||||
|
r = requests.get(url,
|
||||||
|
json=postBody,
|
||||||
|
params=parameters,
|
||||||
|
headers=header,
|
||||||
|
timeout=timeout,
|
||||||
|
cert=cert,
|
||||||
|
verify=verifyssl)
|
||||||
|
|
||||||
|
elif type == "POST":
|
||||||
|
r = requests.post(url,
|
||||||
|
json=postBody,
|
||||||
|
headers=header,
|
||||||
|
timeout=timeout,
|
||||||
|
cert=cert,
|
||||||
|
verify=verifyssl)
|
||||||
|
|
||||||
|
elif type == "DELETE":
|
||||||
|
r = requests.delete(url,
|
||||||
|
json=postBody,
|
||||||
|
headers=header,
|
||||||
|
timeout=timeout,
|
||||||
|
cert=cert,
|
||||||
|
verify=verifyssl)
|
||||||
|
|
||||||
|
# If user is not authenticated
|
||||||
|
elif not authenticate:
|
||||||
|
|
||||||
|
header = self.getHeader(authenticate=False)
|
||||||
|
verifyssl = False
|
||||||
|
|
||||||
|
# If user enables ssl verification
|
||||||
|
try:
|
||||||
|
verifyssl = self.sslverify
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Prepare request
|
||||||
|
if type == "GET":
|
||||||
|
r = requests.get(url,
|
||||||
|
json=postBody,
|
||||||
|
params=parameters,
|
||||||
|
headers=header,
|
||||||
|
timeout=timeout,
|
||||||
|
verify=verifyssl)
|
||||||
|
|
||||||
|
elif type == "POST":
|
||||||
|
r = requests.post(url,
|
||||||
|
json=postBody,
|
||||||
|
headers=header,
|
||||||
|
timeout=timeout,
|
||||||
|
verify=verifyssl)
|
||||||
|
|
||||||
|
##### THE RESPONSE #####
|
||||||
|
self.logMsg(r.url, 2)
|
||||||
|
if r.status_code == 204:
|
||||||
|
# No body in the response
|
||||||
|
self.logMsg("====== 204 Success ======", 2)
|
||||||
|
|
||||||
|
elif r.status_code == requests.codes.ok:
|
||||||
|
|
||||||
|
try:
|
||||||
|
# UTF-8 - JSON object
|
||||||
|
r = r.json()
|
||||||
|
self.logMsg("====== 200 Success ======", 2)
|
||||||
|
self.logMsg("Response: %s" % r, 2)
|
||||||
|
return r
|
||||||
|
|
||||||
|
except:
|
||||||
|
if r.headers.get('content-type') != "text/html":
|
||||||
|
self.logMsg("Unable to convert the response for: %s" % url, 1)
|
||||||
|
else:
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
|
##### EXCEPTIONS #####
|
||||||
|
|
||||||
|
except requests.exceptions.ConnectionError as e:
|
||||||
|
# Make the addon aware of status
|
||||||
|
if utils.window('emby_online') != "false":
|
||||||
|
self.logMsg("Server unreachable at: %s" % url, 0)
|
||||||
|
self.logMsg(e, 2)
|
||||||
|
utils.window('emby_online', value="false")
|
||||||
|
|
||||||
|
except requests.exceptions.ConnectTimeout as e:
|
||||||
|
self.logMsg("Server timeout at: %s" % url, 0)
|
||||||
|
self.logMsg(e, 1)
|
||||||
|
|
||||||
|
except requests.exceptions.HTTPError as e:
|
||||||
|
|
||||||
|
if r.status_code == 401:
|
||||||
|
# Unauthorized
|
||||||
|
status = utils.window('emby_serverStatus')
|
||||||
|
|
||||||
|
if 'X-Application-Error-Code' in r.headers:
|
||||||
|
# Emby server errors
|
||||||
|
if r.headers['X-Application-Error-Code'] == "ParentalControl":
|
||||||
|
# Parental control - access restricted
|
||||||
|
utils.window('emby_serverStatus', value="restricted")
|
||||||
|
xbmcgui.Dialog().notification(
|
||||||
|
heading="Emby server",
|
||||||
|
message="Access restricted.",
|
||||||
|
icon=xbmcgui.NOTIFICATION_ERROR,
|
||||||
|
time=5000)
|
||||||
|
return False
|
||||||
|
|
||||||
|
elif r.headers['X-Application-Error-Code'] == "UnauthorizedAccessException":
|
||||||
|
# User tried to do something his emby account doesn't allow
|
||||||
|
pass
|
||||||
|
|
||||||
|
elif status not in ("401", "Auth"):
|
||||||
|
# Tell userclient token has been revoked.
|
||||||
|
utils.window('emby_serverStatus', value="401")
|
||||||
|
self.logMsg("HTTP Error: %s" % e, 0)
|
||||||
|
xbmcgui.Dialog().notification(
|
||||||
|
heading="Error connecting",
|
||||||
|
message="Unauthorized.",
|
||||||
|
icon=xbmcgui.NOTIFICATION_ERROR)
|
||||||
|
return 401
|
||||||
|
|
||||||
|
elif r.status_code in (301, 302):
|
||||||
|
# Redirects
|
||||||
|
pass
|
||||||
|
elif r.status_code == 400:
|
||||||
|
# Bad requests
|
||||||
|
pass
|
||||||
|
|
||||||
|
except requests.exceptions.SSLError as e:
|
||||||
|
self.logMsg("Invalid SSL certificate for: %s" % url, 0)
|
||||||
|
self.logMsg(e, 1)
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
self.logMsg("Unknown error connecting to: %s" % url, 0)
|
||||||
|
self.logMsg(e, 1)
|
||||||
|
|
||||||
|
return default_link
|
842
resources/lib/entrypoint.py
Normal file
842
resources/lib/entrypoint.py
Normal file
|
@ -0,0 +1,842 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
#################################################################################################
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import urlparse
|
||||||
|
|
||||||
|
import xbmc
|
||||||
|
import xbmcaddon
|
||||||
|
import xbmcgui
|
||||||
|
import xbmcvfs
|
||||||
|
import xbmcplugin
|
||||||
|
|
||||||
|
import artwork
|
||||||
|
import utils
|
||||||
|
import clientinfo
|
||||||
|
import downloadutils
|
||||||
|
import read_embyserver as embyserver
|
||||||
|
import embydb_functions as embydb
|
||||||
|
import playlist
|
||||||
|
import playbackutils as pbutils
|
||||||
|
import playutils
|
||||||
|
import api
|
||||||
|
|
||||||
|
#################################################################################################
|
||||||
|
|
||||||
|
|
||||||
|
def doPlayback(itemid, dbid):
|
||||||
|
|
||||||
|
emby = embyserver.Read_EmbyServer()
|
||||||
|
item = emby.getItem(itemid)
|
||||||
|
pbutils.PlaybackUtils(item).play(itemid, dbid)
|
||||||
|
|
||||||
|
##### DO RESET AUTH #####
|
||||||
|
def resetAuth():
|
||||||
|
# User tried login and failed too many times
|
||||||
|
resp = xbmcgui.Dialog().yesno(
|
||||||
|
heading="Warning",
|
||||||
|
line1=(
|
||||||
|
"Emby might lock your account if you fail to log in too many times. "
|
||||||
|
"Proceed anyway?"))
|
||||||
|
if resp == 1:
|
||||||
|
utils.logMsg("EMBY", "Reset login attempts.", 1)
|
||||||
|
utils.window('emby_serverStatus', value="Auth")
|
||||||
|
else:
|
||||||
|
xbmc.executebuiltin('Addon.OpenSettings(plugin.video.emby)')
|
||||||
|
|
||||||
|
def addDirectoryItem(label, path, folder=True):
|
||||||
|
li = xbmcgui.ListItem(label, path=path)
|
||||||
|
li.setThumbnailImage("special://home/addons/plugin.video.emby/icon.png")
|
||||||
|
li.setArt({"fanart":"special://home/addons/plugin.video.emby/fanart.jpg"})
|
||||||
|
li.setArt({"landscape":"special://home/addons/plugin.video.emby/fanart.jpg"})
|
||||||
|
xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=path, listitem=li, isFolder=folder)
|
||||||
|
|
||||||
|
def doMainListing():
|
||||||
|
|
||||||
|
xbmcplugin.setContent(int(sys.argv[1]), 'files')
|
||||||
|
# Get emby nodes from the window props
|
||||||
|
embyprops = utils.window('Emby.nodes.total')
|
||||||
|
if embyprops:
|
||||||
|
totalnodes = int(embyprops)
|
||||||
|
for i in range(totalnodes):
|
||||||
|
path = utils.window('Emby.nodes.%s.index' % i)
|
||||||
|
if not path:
|
||||||
|
path = utils.window('Emby.nodes.%s.content' % i)
|
||||||
|
label = utils.window('Emby.nodes.%s.title' % i)
|
||||||
|
if path:
|
||||||
|
addDirectoryItem(label, path)
|
||||||
|
|
||||||
|
# some extra entries for settings and stuff. TODO --> localize the labels
|
||||||
|
addDirectoryItem("Network credentials", "plugin://plugin.video.emby/?mode=passwords", False)
|
||||||
|
addDirectoryItem("Settings", "plugin://plugin.video.emby/?mode=settings", False)
|
||||||
|
addDirectoryItem("Add user to session", "plugin://plugin.video.emby/?mode=adduser", False)
|
||||||
|
#addDirectoryItem("Cache all images to Kodi texture cache (advanced)", "plugin://plugin.video.emby/?mode=texturecache")
|
||||||
|
addDirectoryItem("Perform manual sync", "plugin://plugin.video.emby/?mode=manualsync", False)
|
||||||
|
addDirectoryItem(
|
||||||
|
label="Repair local database (force update all content)",
|
||||||
|
path="plugin://plugin.video.emby/?mode=repair",
|
||||||
|
folder=False)
|
||||||
|
addDirectoryItem(
|
||||||
|
label="Perform local database reset (full resync)",
|
||||||
|
path="plugin://plugin.video.emby/?mode=reset",
|
||||||
|
folder=False)
|
||||||
|
addDirectoryItem(
|
||||||
|
label="Sync Emby Theme Media to Kodi",
|
||||||
|
path="plugin://plugin.video.emby/?mode=thememedia",
|
||||||
|
folder=False)
|
||||||
|
|
||||||
|
xbmcplugin.endOfDirectory(int(sys.argv[1]))
|
||||||
|
|
||||||
|
##### ADD ADDITIONAL USERS #####
|
||||||
|
def addUser():
|
||||||
|
|
||||||
|
doUtils = downloadutils.DownloadUtils()
|
||||||
|
clientInfo = clientinfo.ClientInfo()
|
||||||
|
deviceId = clientInfo.getDeviceId()
|
||||||
|
deviceName = clientInfo.getDeviceName()
|
||||||
|
userid = utils.window('emby_currUser')
|
||||||
|
dialog = xbmcgui.Dialog()
|
||||||
|
|
||||||
|
# Get session
|
||||||
|
url = "{server}/emby/Sessions?DeviceId=%s&format=json" % deviceId
|
||||||
|
result = doUtils.downloadUrl(url)
|
||||||
|
|
||||||
|
try:
|
||||||
|
sessionId = result[0]['Id']
|
||||||
|
additionalUsers = result[0]['AdditionalUsers']
|
||||||
|
# Add user to session
|
||||||
|
userlist = {}
|
||||||
|
users = []
|
||||||
|
url = "{server}/emby/Users?IsDisabled=false&IsHidden=false&format=json"
|
||||||
|
result = doUtils.downloadUrl(url)
|
||||||
|
|
||||||
|
# pull the list of users
|
||||||
|
for user in result:
|
||||||
|
name = user['Name']
|
||||||
|
userId = user['Id']
|
||||||
|
if userid != userId:
|
||||||
|
userlist[name] = userId
|
||||||
|
users.append(name)
|
||||||
|
|
||||||
|
# Display dialog if there's additional users
|
||||||
|
if additionalUsers:
|
||||||
|
|
||||||
|
option = dialog.select("Add/Remove user from the session", ["Add user", "Remove user"])
|
||||||
|
# Users currently in the session
|
||||||
|
additionalUserlist = {}
|
||||||
|
additionalUsername = []
|
||||||
|
# Users currently in the session
|
||||||
|
for user in additionalUsers:
|
||||||
|
name = user['UserName']
|
||||||
|
userId = user['UserId']
|
||||||
|
additionalUserlist[name] = userId
|
||||||
|
additionalUsername.append(name)
|
||||||
|
|
||||||
|
if option == 1:
|
||||||
|
# User selected Remove user
|
||||||
|
resp = dialog.select("Remove user from the session", additionalUsername)
|
||||||
|
if resp > -1:
|
||||||
|
selected = additionalUsername[resp]
|
||||||
|
selected_userId = additionalUserlist[selected]
|
||||||
|
url = "{server}/emby/Sessions/%s/Users/%s" % (sessionId, selected_userId)
|
||||||
|
doUtils.downloadUrl(url, postBody={}, type="DELETE")
|
||||||
|
dialog.notification(
|
||||||
|
heading="Success!",
|
||||||
|
message="%s removed from viewing session" % selected,
|
||||||
|
icon="special://home/addons/plugin.video.emby/icon.png",
|
||||||
|
time=1000)
|
||||||
|
|
||||||
|
# clear picture
|
||||||
|
position = utils.window('EmbyAdditionalUserPosition.%s' % selected_userId)
|
||||||
|
utils.window('EmbyAdditionalUserImage.%s' % position, clear=True)
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
elif option == 0:
|
||||||
|
# User selected Add user
|
||||||
|
for adduser in additionalUsername:
|
||||||
|
try: # Remove from selected already added users. It is possible they are hidden.
|
||||||
|
users.remove(adduser)
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
elif option < 0:
|
||||||
|
# User cancelled
|
||||||
|
return
|
||||||
|
|
||||||
|
# Subtract any additional users
|
||||||
|
utils.logMsg("EMBY", "Displaying list of users: %s" % users)
|
||||||
|
resp = dialog.select("Add user to the session", users)
|
||||||
|
# post additional user
|
||||||
|
if resp > -1:
|
||||||
|
selected = users[resp]
|
||||||
|
selected_userId = userlist[selected]
|
||||||
|
url = "{server}/emby/Sessions/%s/Users/%s" % (sessionId, selected_userId)
|
||||||
|
doUtils.downloadUrl(url, postBody={}, type="POST")
|
||||||
|
dialog.notification(
|
||||||
|
heading="Success!",
|
||||||
|
message="%s added to viewing session" % selected,
|
||||||
|
icon="special://home/addons/plugin.video.emby/icon.png",
|
||||||
|
time=1000)
|
||||||
|
|
||||||
|
except:
|
||||||
|
utils.logMsg("EMBY", "Failed to add user to session.")
|
||||||
|
dialog.notification(
|
||||||
|
heading="Error",
|
||||||
|
message="Unable to add/remove user from the session.",
|
||||||
|
icon=xbmcgui.NOTIFICATION_ERROR)
|
||||||
|
|
||||||
|
# Add additional user images
|
||||||
|
# always clear the individual items first
|
||||||
|
totalNodes = 10
|
||||||
|
for i in range(totalNodes):
|
||||||
|
if not utils.window('EmbyAdditionalUserImage.%s' % i):
|
||||||
|
break
|
||||||
|
utils.window('EmbyAdditionalUserImage.%s' % i)
|
||||||
|
|
||||||
|
url = "{server}/emby/Sessions?DeviceId=%s" % deviceId
|
||||||
|
result = doUtils.downloadUrl(url)
|
||||||
|
additionalUsers = result[0]['AdditionalUsers']
|
||||||
|
count = 0
|
||||||
|
for additionaluser in additionalUsers:
|
||||||
|
url = "{server}/emby/Users/%s?format=json" % additionaluser['UserId']
|
||||||
|
result = doUtils.downloadUrl(url)
|
||||||
|
utils.window('EmbyAdditionalUserImage.%s' % count,
|
||||||
|
value=artwork.Artwork().getUserArtwork(result, 'Primary'))
|
||||||
|
utils.window('EmbyAdditionalUserPosition.%s' % additionaluser['UserId'], value=str(count))
|
||||||
|
count +=1
|
||||||
|
|
||||||
|
##### THEME MUSIC/VIDEOS #####
|
||||||
|
def getThemeMedia():
|
||||||
|
|
||||||
|
doUtils = downloadutils.DownloadUtils()
|
||||||
|
dialog = xbmcgui.Dialog()
|
||||||
|
playback = None
|
||||||
|
|
||||||
|
# Choose playback method
|
||||||
|
resp = dialog.select("Playback method for your themes", ["Direct Play", "Direct Stream"])
|
||||||
|
if resp == 0:
|
||||||
|
playback = "DirectPlay"
|
||||||
|
elif resp == 1:
|
||||||
|
playback = "DirectStream"
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
library = xbmc.translatePath(
|
||||||
|
"special://profile/addon_data/plugin.video.emby/library/").decode('utf-8')
|
||||||
|
# Create library directory
|
||||||
|
if not xbmcvfs.exists(library):
|
||||||
|
xbmcvfs.mkdir(library)
|
||||||
|
|
||||||
|
# Set custom path for user
|
||||||
|
tvtunes_path = xbmc.translatePath(
|
||||||
|
"special://profile/addon_data/script.tvtunes/").decode('utf-8')
|
||||||
|
if xbmcvfs.exists(tvtunes_path):
|
||||||
|
tvtunes = xbmcaddon.Addon(id="script.tvtunes")
|
||||||
|
tvtunes.setSetting('custom_path_enable', "true")
|
||||||
|
tvtunes.setSetting('custom_path', library)
|
||||||
|
utils.logMsg("EMBY", "TV Tunes custom path is enabled and set.", 1)
|
||||||
|
else:
|
||||||
|
# if it does not exist this will not work so warn user
|
||||||
|
# often they need to edit the settings first for it to be created.
|
||||||
|
dialog.ok(
|
||||||
|
heading="Warning",
|
||||||
|
line1=(
|
||||||
|
"The settings file does not exist in tvtunes. ",
|
||||||
|
"Go to the tvtunes addon and change a setting, then come back and re-run."))
|
||||||
|
xbmc.executebuiltin('Addon.OpenSettings(script.tvtunes)')
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get every user view Id
|
||||||
|
embyconn = utils.kodiSQL('emby')
|
||||||
|
embycursor = embyconn.cursor()
|
||||||
|
emby_db = embydb.Embydb_Functions(embycursor)
|
||||||
|
viewids = emby_db.getViews()
|
||||||
|
embycursor.close()
|
||||||
|
|
||||||
|
# Get Ids with Theme Videos
|
||||||
|
itemIds = {}
|
||||||
|
for view in viewids:
|
||||||
|
url = "{server}/emby/Users/{UserId}/Items?HasThemeVideo=True&ParentId=%s&format=json" % view
|
||||||
|
result = doUtils.downloadUrl(url)
|
||||||
|
if result['TotalRecordCount'] != 0:
|
||||||
|
for item in result['Items']:
|
||||||
|
itemId = item['Id']
|
||||||
|
folderName = item['Name']
|
||||||
|
folderName = utils.normalize_string(folderName.encode('utf-8'))
|
||||||
|
itemIds[itemId] = folderName
|
||||||
|
|
||||||
|
# Get paths for theme videos
|
||||||
|
for itemId in itemIds:
|
||||||
|
nfo_path = xbmc.translatePath(
|
||||||
|
"special://profile/addon_data/plugin.video.emby/library/%s/" % itemIds[itemId])
|
||||||
|
# Create folders for each content
|
||||||
|
if not xbmcvfs.exists(nfo_path):
|
||||||
|
xbmcvfs.mkdir(nfo_path)
|
||||||
|
# Where to put the nfos
|
||||||
|
nfo_path = "%s%s" % (nfo_path, "tvtunes.nfo")
|
||||||
|
|
||||||
|
url = "{server}/emby/Items/%s/ThemeVideos?format=json" % itemId
|
||||||
|
result = doUtils.downloadUrl(url)
|
||||||
|
|
||||||
|
# Create nfo and write themes to it
|
||||||
|
nfo_file = open(nfo_path, 'w')
|
||||||
|
pathstowrite = ""
|
||||||
|
# May be more than one theme
|
||||||
|
for theme in result['Items']:
|
||||||
|
putils = playutils.PlayUtils(theme)
|
||||||
|
if playback == "DirectPlay":
|
||||||
|
playurl = putils.directPlay()
|
||||||
|
else:
|
||||||
|
playurl = putils.directStream()
|
||||||
|
pathstowrite += ('<file>%s</file>' % playurl.encode('utf-8'))
|
||||||
|
|
||||||
|
# Check if the item has theme songs and add them
|
||||||
|
url = "{server}/emby/Items/%s/ThemeSongs?format=json" % itemId
|
||||||
|
result = doUtils.downloadUrl(url)
|
||||||
|
|
||||||
|
# May be more than one theme
|
||||||
|
for theme in result['Items']:
|
||||||
|
putils = playutils.PlayUtils(theme)
|
||||||
|
if playback == "DirectPlay":
|
||||||
|
playurl = putils.directPlay()
|
||||||
|
else:
|
||||||
|
playurl = putils.directStream()
|
||||||
|
pathstowrite += ('<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 viewids:
|
||||||
|
url = "{server}/emby/Users/{UserId}/Items?HasThemeSong=True&ParentId=%s&format=json" % view
|
||||||
|
result = doUtils.downloadUrl(url)
|
||||||
|
if result['TotalRecordCount'] != 0:
|
||||||
|
for item in result['Items']:
|
||||||
|
itemId = item['Id']
|
||||||
|
folderName = item['Name']
|
||||||
|
folderName = utils.normalize_string(folderName.encode('utf-8'))
|
||||||
|
musicitemIds[itemId] = folderName
|
||||||
|
|
||||||
|
# Get paths
|
||||||
|
for itemId in musicitemIds:
|
||||||
|
|
||||||
|
# if the item was already processed with video themes back out
|
||||||
|
if itemId in itemIds:
|
||||||
|
continue
|
||||||
|
|
||||||
|
nfo_path = xbmc.translatePath(
|
||||||
|
"special://profile/addon_data/plugin.video.emby/library/%s/" % musicitemIds[itemId])
|
||||||
|
# Create folders for each content
|
||||||
|
if not xbmcvfs.exists(nfo_path):
|
||||||
|
xbmcvfs.mkdir(nfo_path)
|
||||||
|
# Where to put the nfos
|
||||||
|
nfo_path = "%s%s" % (nfo_path, "tvtunes.nfo")
|
||||||
|
|
||||||
|
url = "{server}/emby/Items/%s/ThemeSongs?format=json" % itemId
|
||||||
|
result = doUtils.downloadUrl(url)
|
||||||
|
|
||||||
|
# Create nfo and write themes to it
|
||||||
|
nfo_file = open(nfo_path, 'w')
|
||||||
|
pathstowrite = ""
|
||||||
|
# May be more than one theme
|
||||||
|
for theme in result['Items']:
|
||||||
|
putils = playutils.PlayUtils(theme)
|
||||||
|
if playback == "DirectPlay":
|
||||||
|
playurl = putils.directPlay()
|
||||||
|
else:
|
||||||
|
playurl = putils.directStream()
|
||||||
|
pathstowrite += ('<file>%s</file>' % playurl.encode('utf-8'))
|
||||||
|
|
||||||
|
nfo_file.write(
|
||||||
|
'<tvtunes>%s</tvtunes>' % pathstowrite
|
||||||
|
)
|
||||||
|
# Close nfo file
|
||||||
|
nfo_file.close()
|
||||||
|
|
||||||
|
dialog.notification(
|
||||||
|
heading="Emby for Kodi",
|
||||||
|
message="Themes added!",
|
||||||
|
icon="special://home/addons/plugin.video.emby/icon.png",
|
||||||
|
time=1000,
|
||||||
|
sound=False)
|
||||||
|
|
||||||
|
##### BROWSE EMBY CHANNELS #####
|
||||||
|
def BrowseChannels(itemid, folderid=None):
|
||||||
|
|
||||||
|
_addon_id = int(sys.argv[1])
|
||||||
|
_addon_url = sys.argv[0]
|
||||||
|
doUtils = downloadutils.DownloadUtils()
|
||||||
|
art = artwork.Artwork()
|
||||||
|
|
||||||
|
xbmcplugin.setContent(int(sys.argv[1]), 'files')
|
||||||
|
if folderid:
|
||||||
|
url = (
|
||||||
|
"{server}/emby/Channels/%s/Items?userid={UserId}&folderid=%s&format=json"
|
||||||
|
% (itemid, folderid))
|
||||||
|
elif itemid == "0":
|
||||||
|
# id 0 is the root channels folder
|
||||||
|
url = "{server}/emby/Channels?{UserId}&format=json"
|
||||||
|
else:
|
||||||
|
url = "{server}/emby/Channels/%s/Items?UserId={UserId}&format=json" % itemid
|
||||||
|
|
||||||
|
result = doUtils.downloadUrl(url)
|
||||||
|
try:
|
||||||
|
channels = result['Items']
|
||||||
|
except TypeError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
for item in channels:
|
||||||
|
|
||||||
|
API = api.API(item)
|
||||||
|
itemid = item['Id']
|
||||||
|
itemtype = item['Type']
|
||||||
|
title = item.get('Name', "Missing Title")
|
||||||
|
li = xbmcgui.ListItem(title)
|
||||||
|
|
||||||
|
if itemtype == "ChannelFolderItem":
|
||||||
|
isFolder = True
|
||||||
|
else:
|
||||||
|
isFolder = False
|
||||||
|
|
||||||
|
channelId = item.get('ChannelId', "")
|
||||||
|
channelName = item.get('ChannelName', "")
|
||||||
|
|
||||||
|
premieredate = API.getPremiereDate()
|
||||||
|
# Process Genres
|
||||||
|
genre = API.getGenres()
|
||||||
|
# Process UserData
|
||||||
|
overlay = 0
|
||||||
|
|
||||||
|
userdata = API.getUserData()
|
||||||
|
seektime = userdata['Resume']
|
||||||
|
played = userdata['Played']
|
||||||
|
if played:
|
||||||
|
overlay = 7
|
||||||
|
else:
|
||||||
|
overlay = 6
|
||||||
|
|
||||||
|
favorite = userdata['Favorite']
|
||||||
|
if favorite:
|
||||||
|
overlay = 5
|
||||||
|
|
||||||
|
playcount = userdata['PlayCount']
|
||||||
|
if playcount is None:
|
||||||
|
playcount = 0
|
||||||
|
|
||||||
|
# Populate the details list
|
||||||
|
details = {
|
||||||
|
|
||||||
|
'title': title,
|
||||||
|
'channelname': channelName,
|
||||||
|
'plot': API.getOverview(),
|
||||||
|
'Overlay': str(overlay),
|
||||||
|
'playcount': str(playcount)
|
||||||
|
}
|
||||||
|
|
||||||
|
if itemtype == "ChannelVideoItem":
|
||||||
|
xbmcplugin.setContent(_addon_id, 'movies')
|
||||||
|
elif itemtype == "ChannelAudioItem":
|
||||||
|
xbmcplugin.setContent(_addon_id, 'songs')
|
||||||
|
|
||||||
|
# Populate the extradata list and artwork
|
||||||
|
pbutils.PlaybackUtils(item).setArtwork(li)
|
||||||
|
extradata = {
|
||||||
|
|
||||||
|
'id': itemid,
|
||||||
|
'rating': item.get('CommunityRating'),
|
||||||
|
'year': item.get('ProductionYear'),
|
||||||
|
'premieredate': premieredate,
|
||||||
|
'genre': genre,
|
||||||
|
'playcount': str(playcount),
|
||||||
|
'itemtype': itemtype
|
||||||
|
}
|
||||||
|
li.setInfo('video', infoLabels=extradata)
|
||||||
|
li.setThumbnailImage(art.getAllArtwork(item)['Primary'])
|
||||||
|
li.setIconImage('DefaultTVShows.png')
|
||||||
|
|
||||||
|
if itemtype == "Channel":
|
||||||
|
path = "%s?id=%s&mode=channels" % (_addon_url, itemid)
|
||||||
|
xbmcplugin.addDirectoryItem(handle=_addon_id, url=path, listitem=li, isFolder=True)
|
||||||
|
|
||||||
|
elif isFolder:
|
||||||
|
path = "%s?id=%s&mode=channelsfolder&folderid=%s" % (_addon_url, channelId, itemid)
|
||||||
|
xbmcplugin.addDirectoryItem(handle=_addon_id, url=path, listitem=li, isFolder=True)
|
||||||
|
else:
|
||||||
|
path = "%s?id=%s&mode=play" % (_addon_url, itemid)
|
||||||
|
li.setProperty('IsPlayable', 'true')
|
||||||
|
xbmcplugin.addDirectoryItem(handle=_addon_id, url=path, listitem=li)
|
||||||
|
|
||||||
|
xbmcplugin.endOfDirectory(handle=int(sys.argv[1]))
|
||||||
|
|
||||||
|
##### LISTITEM SETUP FOR VIDEONODES #####
|
||||||
|
def createListItem(item):
|
||||||
|
|
||||||
|
title = item['title']
|
||||||
|
li = xbmcgui.ListItem(title)
|
||||||
|
li.setProperty('IsPlayable', "true")
|
||||||
|
|
||||||
|
metadata = {
|
||||||
|
|
||||||
|
'Title': title,
|
||||||
|
'duration': str(item['runtime']/60),
|
||||||
|
'Plot': item['plot'],
|
||||||
|
'Playcount': item['playcount']
|
||||||
|
}
|
||||||
|
|
||||||
|
if "episode" in item:
|
||||||
|
episode = item['episode']
|
||||||
|
metadata['Episode'] = episode
|
||||||
|
|
||||||
|
if "season" in item:
|
||||||
|
season = item['season']
|
||||||
|
metadata['Season'] = season
|
||||||
|
|
||||||
|
if season and episode:
|
||||||
|
li.setProperty('episodeno', "s%.2de%.2d" % (season, episode))
|
||||||
|
|
||||||
|
if "firstaired" in item:
|
||||||
|
metadata['Premiered'] = item['firstaired']
|
||||||
|
|
||||||
|
if "showtitle" in item:
|
||||||
|
metadata['TVshowTitle'] = item['showtitle']
|
||||||
|
|
||||||
|
if "rating" in item:
|
||||||
|
metadata['Rating'] = str(round(float(item['rating']),1))
|
||||||
|
|
||||||
|
if "director" in item:
|
||||||
|
metadata['Director'] = " / ".join(item['director'])
|
||||||
|
|
||||||
|
if "writer" in item:
|
||||||
|
metadata['Writer'] = " / ".join(item['writer'])
|
||||||
|
|
||||||
|
if "cast" in item:
|
||||||
|
cast = []
|
||||||
|
castandrole = []
|
||||||
|
for person in item['cast']:
|
||||||
|
name = person['name']
|
||||||
|
cast.append(name)
|
||||||
|
castandrole.append((name, person['role']))
|
||||||
|
metadata['Cast'] = cast
|
||||||
|
metadata['CastAndRole'] = castandrole
|
||||||
|
|
||||||
|
li.setInfo(type="Video", infoLabels=metadata)
|
||||||
|
li.setProperty('resumetime', str(item['resume']['position']))
|
||||||
|
li.setProperty('totaltime', str(item['resume']['total']))
|
||||||
|
li.setArt(item['art'])
|
||||||
|
li.setThumbnailImage(item['art'].get('thumb',''))
|
||||||
|
li.setIconImage('DefaultTVShows.png')
|
||||||
|
li.setProperty('dbid', str(item['episodeid']))
|
||||||
|
li.setProperty('fanart_image', item['art'].get('tvshow.fanart',''))
|
||||||
|
for key, value in item['streamdetails'].iteritems():
|
||||||
|
for stream in value:
|
||||||
|
li.addStreamInfo(key, stream)
|
||||||
|
|
||||||
|
return li
|
||||||
|
|
||||||
|
##### GET NEXTUP EPISODES FOR TAGNAME #####
|
||||||
|
def getNextUpEpisodes(tagname, limit):
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
# if the addon is called with nextup parameter,
|
||||||
|
# we return the nextepisodes list of the given tagname
|
||||||
|
xbmcplugin.setContent(int(sys.argv[1]), 'episodes')
|
||||||
|
# First we get a list of all the TV shows - filtered by tag
|
||||||
|
query = {
|
||||||
|
|
||||||
|
'jsonrpc': "2.0",
|
||||||
|
'id': "libTvShows",
|
||||||
|
'method': "VideoLibrary.GetTVShows",
|
||||||
|
'params': {
|
||||||
|
|
||||||
|
'sort': {'order': "descending", 'method': "lastplayed"},
|
||||||
|
'filter': {
|
||||||
|
'and': [
|
||||||
|
{'operator': "true", 'field': "inprogress", 'value': ""},
|
||||||
|
{'operator': "contains", 'field': "tag", 'value': "%s" % tagname}
|
||||||
|
]},
|
||||||
|
'properties': ['title', 'studio', 'mpaa', 'file', 'art']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = xbmc.executeJSONRPC(json.dumps(query))
|
||||||
|
result = json.loads(result)
|
||||||
|
# If we found any, find the oldest unwatched show for each one.
|
||||||
|
try:
|
||||||
|
items = result['result']['tvshows']
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
for item in items:
|
||||||
|
if utils.settings('ignoreSpecialsNextEpisodes') == "true":
|
||||||
|
query = {
|
||||||
|
|
||||||
|
'jsonrpc': "2.0",
|
||||||
|
'id': 1,
|
||||||
|
'method': "VideoLibrary.GetEpisodes",
|
||||||
|
'params': {
|
||||||
|
|
||||||
|
'tvshowid': item['tvshowid'],
|
||||||
|
'sort': {'method': "episode"},
|
||||||
|
'filter': {
|
||||||
|
'and': [
|
||||||
|
{'operator': "lessthan", 'field': "playcount", 'value': "1"},
|
||||||
|
{'operator': "greaterthan", 'field': "season", 'value': "0"}
|
||||||
|
]},
|
||||||
|
'properties': [
|
||||||
|
"title", "playcount", "season", "episode", "showtitle",
|
||||||
|
"plot", "file", "rating", "resume", "tvshowid", "art",
|
||||||
|
"streamdetails", "firstaired", "runtime", "writer",
|
||||||
|
"dateadded", "lastplayed"
|
||||||
|
],
|
||||||
|
'limits': {"end": 1}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
query = {
|
||||||
|
|
||||||
|
'jsonrpc': "2.0",
|
||||||
|
'id': 1,
|
||||||
|
'method': "VideoLibrary.GetEpisodes",
|
||||||
|
'params': {
|
||||||
|
|
||||||
|
'tvshowid': item['tvshowid'],
|
||||||
|
'sort': {'method': "episode"},
|
||||||
|
'filter': {'operator': "lessthan", 'field': "playcount", 'value': "1"},
|
||||||
|
'properties': [
|
||||||
|
"title", "playcount", "season", "episode", "showtitle",
|
||||||
|
"plot", "file", "rating", "resume", "tvshowid", "art",
|
||||||
|
"streamdetails", "firstaired", "runtime", "writer",
|
||||||
|
"dateadded", "lastplayed"
|
||||||
|
],
|
||||||
|
'limits': {"end": 1}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = xbmc.executeJSONRPC(json.dumps(query))
|
||||||
|
result = json.loads(result)
|
||||||
|
try:
|
||||||
|
episodes = result['result']['episodes']
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
for episode in episodes:
|
||||||
|
li = createListItem(episode)
|
||||||
|
xbmcplugin.addDirectoryItem(
|
||||||
|
handle=int(sys.argv[1]),
|
||||||
|
url=item['file'],
|
||||||
|
listitem=li)
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
if count == limit:
|
||||||
|
break
|
||||||
|
|
||||||
|
xbmcplugin.endOfDirectory(handle=int(sys.argv[1]))
|
||||||
|
|
||||||
|
##### GET INPROGRESS EPISODES FOR TAGNAME #####
|
||||||
|
def getInProgressEpisodes(tagname, limit):
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
# if the addon is called with inprogressepisodes parameter,
|
||||||
|
# we return the inprogressepisodes list of the given tagname
|
||||||
|
xbmcplugin.setContent(int(sys.argv[1]), 'episodes')
|
||||||
|
# First we get a list of all the in-progress TV shows - filtered by tag
|
||||||
|
query = {
|
||||||
|
|
||||||
|
'jsonrpc': "2.0",
|
||||||
|
'id': "libTvShows",
|
||||||
|
'method': "VideoLibrary.GetTVShows",
|
||||||
|
'params': {
|
||||||
|
|
||||||
|
'sort': {'order': "descending", 'method': "lastplayed"},
|
||||||
|
'filter': {
|
||||||
|
'and': [
|
||||||
|
{'operator': "true", 'field': "inprogress", 'value': ""},
|
||||||
|
{'operator': "contains", 'field': "tag", 'value': "%s" % tagname}
|
||||||
|
]},
|
||||||
|
'properties': ['title', 'studio', 'mpaa', 'file', 'art']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = xbmc.executeJSONRPC(json.dumps(query))
|
||||||
|
result = json.loads(result)
|
||||||
|
# If we found any, find the oldest unwatched show for each one.
|
||||||
|
try:
|
||||||
|
items = result['result']['tvshows']
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
for item in items:
|
||||||
|
query = {
|
||||||
|
|
||||||
|
'jsonrpc': "2.0",
|
||||||
|
'id': 1,
|
||||||
|
'method': "VideoLibrary.GetEpisodes",
|
||||||
|
'params': {
|
||||||
|
|
||||||
|
'tvshowid': item['tvshowid'],
|
||||||
|
'sort': {'method': "episode"},
|
||||||
|
'filter': {'operator': "true", 'field': "inprogress", 'value': ""},
|
||||||
|
'properties': [
|
||||||
|
"title", "playcount", "season", "episode", "showtitle", "plot",
|
||||||
|
"file", "rating", "resume", "tvshowid", "art", "cast",
|
||||||
|
"streamdetails", "firstaired", "runtime", "writer",
|
||||||
|
"dateadded", "lastplayed"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = xbmc.executeJSONRPC(json.dumps(query))
|
||||||
|
result = json.loads(result)
|
||||||
|
try:
|
||||||
|
episodes = result['result']['episodes']
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
for episode in episodes:
|
||||||
|
li = createListItem(episode)
|
||||||
|
xbmcplugin.addDirectoryItem(
|
||||||
|
handle=int(sys.argv[1]),
|
||||||
|
url=item['file'],
|
||||||
|
listitem=li)
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
if count == limit:
|
||||||
|
break
|
||||||
|
|
||||||
|
xbmcplugin.endOfDirectory(handle=int(sys.argv[1]))
|
||||||
|
|
||||||
|
##### GET RECENT EPISODES FOR TAGNAME #####
|
||||||
|
def getRecentEpisodes(tagname, limit):
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
# if the addon is called with recentepisodes parameter,
|
||||||
|
# we return the recentepisodes list of the given tagname
|
||||||
|
xbmcplugin.setContent(int(sys.argv[1]), 'episodes')
|
||||||
|
# First we get a list of all the TV shows - filtered by tag
|
||||||
|
query = {
|
||||||
|
|
||||||
|
'jsonrpc': "2.0",
|
||||||
|
'id': "libTvShows",
|
||||||
|
'method': "VideoLibrary.GetTVShows",
|
||||||
|
'params': {
|
||||||
|
|
||||||
|
'sort': {'order': "descending", 'method': "dateadded"},
|
||||||
|
'filter': {'operator': "contains", 'field': "tag", 'value': "%s" % tagname},
|
||||||
|
'properties': ["title","sorttitle"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = xbmc.executeJSONRPC(json.dumps(query))
|
||||||
|
result = json.loads(result)
|
||||||
|
# If we found any, find the oldest unwatched show for each one.
|
||||||
|
try:
|
||||||
|
items = result['result']['tvshows']
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
allshowsIds = set()
|
||||||
|
for item in items:
|
||||||
|
allshowsIds.add(item['tvshowid'])
|
||||||
|
|
||||||
|
query = {
|
||||||
|
|
||||||
|
'jsonrpc': "2.0",
|
||||||
|
'id': 1,
|
||||||
|
'method': "VideoLibrary.GetEpisodes",
|
||||||
|
'params': {
|
||||||
|
|
||||||
|
'sort': {'order': "descending", 'method': "dateadded"},
|
||||||
|
'filter': {'operator': "lessthan", 'field': "playcount", 'value': "1"},
|
||||||
|
'properties': [
|
||||||
|
"title", "playcount", "season", "episode", "showtitle", "plot",
|
||||||
|
"file", "rating", "resume", "tvshowid", "art", "streamdetails",
|
||||||
|
"firstaired", "runtime", "cast", "writer", "dateadded", "lastplayed"
|
||||||
|
],
|
||||||
|
"limits": {"end": limit}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = xbmc.executeJSONRPC(json.dumps(query))
|
||||||
|
result = json.loads(result)
|
||||||
|
try:
|
||||||
|
episodes = result['result']['episodes']
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
for episode in episodes:
|
||||||
|
if episode['tvshowid'] in allshowsIds:
|
||||||
|
li = createListItem(episode)
|
||||||
|
xbmcplugin.addDirectoryItem(
|
||||||
|
handle=int(sys.argv[1]),
|
||||||
|
url=item['file'],
|
||||||
|
listitem=li)
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
if count == limit:
|
||||||
|
break
|
||||||
|
|
||||||
|
xbmcplugin.endOfDirectory(handle=int(sys.argv[1]))
|
||||||
|
|
||||||
|
##### GET EXTRAFANART FOR LISTITEM #####
|
||||||
|
def getExtraFanArt():
|
||||||
|
|
||||||
|
emby = embyserver.Read_EmbyServer()
|
||||||
|
art = artwork.Artwork()
|
||||||
|
|
||||||
|
# Get extrafanart for listitem
|
||||||
|
# this will only be used for skins that actually call the listitem's path + fanart dir...
|
||||||
|
try:
|
||||||
|
# Only do this if the listitem has actually changed
|
||||||
|
itemPath = xbmc.getInfoLabel("ListItem.FileNameAndPath")
|
||||||
|
|
||||||
|
if not itemPath:
|
||||||
|
itemPath = xbmc.getInfoLabel("ListItem.Path")
|
||||||
|
|
||||||
|
if any([x in itemPath for x in ['tvshows', 'musicvideos', 'movies']]):
|
||||||
|
params = urlparse.parse_qs(itemPath)
|
||||||
|
embyId = params['id'][0]
|
||||||
|
|
||||||
|
utils.logMsg("EMBY", "Requesting extrafanart for Id: %s" % embyId, 1)
|
||||||
|
|
||||||
|
# We need to store the images locally for this to work
|
||||||
|
# because of the caching system in xbmc
|
||||||
|
fanartDir = xbmc.translatePath("special://thumbnails/emby/%s/" % embyId).decode('utf-8')
|
||||||
|
|
||||||
|
if not xbmcvfs.exists(fanartDir):
|
||||||
|
# Download the images to the cache directory
|
||||||
|
xbmcvfs.mkdirs(fanartDir)
|
||||||
|
item = emby.getItem(embyId)
|
||||||
|
if item:
|
||||||
|
backdrops = art.getAllArtwork(item)['Backdrop']
|
||||||
|
tags = item['BackdropImageTags']
|
||||||
|
count = 0
|
||||||
|
for backdrop in backdrops:
|
||||||
|
# Same ordering as in artwork
|
||||||
|
tag = tags[count]
|
||||||
|
fanartFile = os.path.join(fanartDir, "fanart%s.jpg" % tag)
|
||||||
|
li = xbmcgui.ListItem(tag, path=fanartFile)
|
||||||
|
xbmcplugin.addDirectoryItem(
|
||||||
|
handle=int(sys.argv[1]),
|
||||||
|
url=fanartFile,
|
||||||
|
listitem=li)
|
||||||
|
xbmcvfs.copy(backdrop, fanartFile)
|
||||||
|
count += 1
|
||||||
|
else:
|
||||||
|
utils.logMsg("EMBY", "Found cached backdrop.", 2)
|
||||||
|
# Use existing cached images
|
||||||
|
dirs, files = xbmcvfs.listdir(fanartDir)
|
||||||
|
for file in files:
|
||||||
|
fanartFile = os.path.join(fanartDir, file)
|
||||||
|
li = xbmcgui.ListItem(file, path=fanartFile)
|
||||||
|
xbmcplugin.addDirectoryItem(
|
||||||
|
handle=int(sys.argv[1]),
|
||||||
|
url=fanartFile,
|
||||||
|
listitem=li)
|
||||||
|
except Exception as e:
|
||||||
|
utils.logMsg("EMBY", "Error getting extrafanart: %s" % e, 1)
|
||||||
|
|
||||||
|
# Always do endofdirectory to prevent errors in the logs
|
||||||
|
xbmcplugin.endOfDirectory(int(sys.argv[1]))
|
195
resources/lib/kodimonitor.py
Normal file
195
resources/lib/kodimonitor.py
Normal file
|
@ -0,0 +1,195 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
#################################################################################################
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
import xbmc
|
||||||
|
import xbmcgui
|
||||||
|
|
||||||
|
import clientinfo
|
||||||
|
import downloadutils
|
||||||
|
import embydb_functions as embydb
|
||||||
|
import playbackutils as pbutils
|
||||||
|
import utils
|
||||||
|
|
||||||
|
#################################################################################################
|
||||||
|
|
||||||
|
|
||||||
|
class KodiMonitor(xbmc.Monitor):
|
||||||
|
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
|
||||||
|
self.clientInfo = clientinfo.ClientInfo()
|
||||||
|
self.addonName = self.clientInfo.getAddonName()
|
||||||
|
self.doUtils = downloadutils.DownloadUtils()
|
||||||
|
|
||||||
|
self.logMsg("Kodi monitor started.", 1)
|
||||||
|
|
||||||
|
def logMsg(self, msg, lvl=1):
|
||||||
|
|
||||||
|
self.className = self.__class__.__name__
|
||||||
|
utils.logMsg("%s %s" % (self.addonName, self.className), msg, lvl)
|
||||||
|
|
||||||
|
|
||||||
|
def onScanStarted(self, library):
|
||||||
|
self.logMsg("Kodi library scan %s running." % library, 2)
|
||||||
|
if library == "video":
|
||||||
|
utils.window('emby_kodiScan', value="true")
|
||||||
|
|
||||||
|
def onScanFinished(self, library):
|
||||||
|
self.logMsg("Kodi library scan %s finished." % library, 2)
|
||||||
|
if library == "video":
|
||||||
|
utils.window('emby_kodiScan', clear=True)
|
||||||
|
|
||||||
|
def onNotification(self, sender, method, data):
|
||||||
|
|
||||||
|
doUtils = self.doUtils
|
||||||
|
if method not in ("Playlist.OnAdd"):
|
||||||
|
self.logMsg("Method: %s Data: %s" % (method, data), 1)
|
||||||
|
|
||||||
|
if data:
|
||||||
|
data = json.loads(data)
|
||||||
|
|
||||||
|
|
||||||
|
if method == "Player.OnPlay":
|
||||||
|
# Set up report progress for emby playback
|
||||||
|
item = data.get('item')
|
||||||
|
try:
|
||||||
|
kodiid = item['id']
|
||||||
|
type = item['type']
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
self.logMsg("Properties already set for item.", 1)
|
||||||
|
else:
|
||||||
|
if ((utils.settings('useDirectPaths') == "1" and not type == "song") or
|
||||||
|
(type == "song" and utils.settings('disableMusic') == "false")):
|
||||||
|
# Set up properties for player
|
||||||
|
embyconn = utils.kodiSQL('emby')
|
||||||
|
embycursor = embyconn.cursor()
|
||||||
|
emby_db = embydb.Embydb_Functions(embycursor)
|
||||||
|
emby_dbitem = emby_db.getItem_byKodiId(kodiid, type)
|
||||||
|
try:
|
||||||
|
itemid = emby_dbitem[0]
|
||||||
|
except TypeError:
|
||||||
|
self.logMsg("No kodiid returned.", 1)
|
||||||
|
else:
|
||||||
|
url = "{server}/emby/Users/{UserId}/Items/%s?format=json" % itemid
|
||||||
|
result = doUtils.downloadUrl(url)
|
||||||
|
self.logMsg("Item: %s" % result, 2)
|
||||||
|
|
||||||
|
playurl = None
|
||||||
|
count = 0
|
||||||
|
while not playurl and count < 2:
|
||||||
|
try:
|
||||||
|
playurl = xbmc.Player().getPlayingFile()
|
||||||
|
except RuntimeError:
|
||||||
|
count += 1
|
||||||
|
xbmc.sleep(200)
|
||||||
|
else:
|
||||||
|
listItem = xbmcgui.ListItem()
|
||||||
|
playback = pbutils.PlaybackUtils(result)
|
||||||
|
|
||||||
|
if type == "song" and utils.settings('streamMusic') == "true":
|
||||||
|
utils.window('emby_%s.playmethod' % playurl,
|
||||||
|
value="DirectStream")
|
||||||
|
else:
|
||||||
|
utils.window('emby_%s.playmethod' % playurl,
|
||||||
|
value="DirectPlay")
|
||||||
|
# Set properties for player.py
|
||||||
|
playback.setProperties(playurl, listItem)
|
||||||
|
finally:
|
||||||
|
embycursor.close()
|
||||||
|
|
||||||
|
|
||||||
|
elif method == "VideoLibrary.OnUpdate":
|
||||||
|
# Manually marking as watched/unwatched
|
||||||
|
playcount = data.get('playcount')
|
||||||
|
item = data.get('item')
|
||||||
|
try:
|
||||||
|
kodiid = item['id']
|
||||||
|
type = item['type']
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
self.logMsg("Item is invalid for playstate update.", 1)
|
||||||
|
else:
|
||||||
|
# Send notification to the server.
|
||||||
|
embyconn = utils.kodiSQL('emby')
|
||||||
|
embycursor = embyconn.cursor()
|
||||||
|
emby_db = embydb.Embydb_Functions(embycursor)
|
||||||
|
emby_dbitem = emby_db.getItem_byKodiId(kodiid, type)
|
||||||
|
try:
|
||||||
|
itemid = emby_dbitem[0]
|
||||||
|
except TypeError:
|
||||||
|
self.logMsg("Could not find itemid in emby database.", 1)
|
||||||
|
else:
|
||||||
|
# Stop from manually marking as watched unwatched, with actual playback.
|
||||||
|
if utils.window('emby_skipWatched%s' % itemid) == "true":
|
||||||
|
# property is set in player.py
|
||||||
|
utils.window('emby_skipWatched%s' % itemid, clear=True)
|
||||||
|
else:
|
||||||
|
# notify the server
|
||||||
|
url = "{server}/emby/Users/{UserId}/PlayedItems/%s?format=json" % itemid
|
||||||
|
if playcount != 0:
|
||||||
|
doUtils.downloadUrl(url, type="POST")
|
||||||
|
self.logMsg("Mark as watched for itemid: %s" % itemid, 1)
|
||||||
|
else:
|
||||||
|
doUtils.downloadUrl(url, type="DELETE")
|
||||||
|
self.logMsg("Mark as unwatched for itemid: %s" % itemid, 1)
|
||||||
|
finally:
|
||||||
|
embycursor.close()
|
||||||
|
|
||||||
|
|
||||||
|
elif method == "VideoLibrary.OnRemove":
|
||||||
|
|
||||||
|
try:
|
||||||
|
kodiid = data['id']
|
||||||
|
type = data['type']
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
self.logMsg("Item is invalid for emby deletion.", 1)
|
||||||
|
else:
|
||||||
|
# Send the delete action to the server.
|
||||||
|
offerDelete = False
|
||||||
|
|
||||||
|
if type == "episode" and utils.settings('deleteTV') == "true":
|
||||||
|
offerDelete = True
|
||||||
|
elif type == "movie" and utils.settings('deleteMovies') == "true":
|
||||||
|
offerDelete = True
|
||||||
|
|
||||||
|
if utils.settings('offerDelete') != "true":
|
||||||
|
# Delete could be disabled, even if the subsetting is enabled.
|
||||||
|
offerDelete = False
|
||||||
|
|
||||||
|
if offerDelete:
|
||||||
|
embyconn = utils.kodiSQL('emby')
|
||||||
|
embycursor = embyconn.cursor()
|
||||||
|
emby_db = embydb.Embydb_Functions(embycursor)
|
||||||
|
emby_dbitem = emby_db.getItem_byKodiId(kodiid, type)
|
||||||
|
try:
|
||||||
|
itemid = emby_dbitem[0]
|
||||||
|
except TypeError:
|
||||||
|
self.logMsg("Could not find itemid in emby database.", 1)
|
||||||
|
else:
|
||||||
|
if utils.settings('skipConfirmDelete') != "true":
|
||||||
|
resp = xbmcgui.Dialog().yesno(
|
||||||
|
heading="Confirm delete",
|
||||||
|
line1="Delete file on Emby Server?")
|
||||||
|
if not resp:
|
||||||
|
self.logMsg("User skipped deletion.", 1)
|
||||||
|
embycursor.close()
|
||||||
|
return
|
||||||
|
url = "{server}/emby/Items/%s?format=json" % itemid
|
||||||
|
self.logMsg("Deleting request: %s" % itemid)
|
||||||
|
doUtils.downloadUrl(url, type="DELETE")
|
||||||
|
finally:
|
||||||
|
embycursor.close()
|
||||||
|
|
||||||
|
|
||||||
|
elif method == "System.OnWake":
|
||||||
|
# Allow network to wake up
|
||||||
|
xbmc.sleep(10000)
|
||||||
|
utils.window('emby_onWake', value="true")
|
||||||
|
|
||||||
|
elif method == "Playlist.OnClear":
|
||||||
|
utils.window('emby_customPlaylist', clear=True, windowid=10101)
|
||||||
|
#xbmcgui.Window(10101).clearProperties()
|
||||||
|
self.logMsg("Clear playlist properties.")
|
1338
resources/lib/librarysync.py
Normal file
1338
resources/lib/librarysync.py
Normal file
File diff suppressed because it is too large
Load diff
353
resources/lib/playbackutils.py
Normal file
353
resources/lib/playbackutils.py
Normal file
|
@ -0,0 +1,353 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
#################################################################################################
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import xbmc
|
||||||
|
import xbmcgui
|
||||||
|
import xbmcplugin
|
||||||
|
|
||||||
|
import api
|
||||||
|
import artwork
|
||||||
|
import clientinfo
|
||||||
|
import downloadutils
|
||||||
|
import playutils as putils
|
||||||
|
import playlist
|
||||||
|
import read_embyserver as embyserver
|
||||||
|
import utils
|
||||||
|
|
||||||
|
#################################################################################################
|
||||||
|
|
||||||
|
|
||||||
|
class PlaybackUtils():
|
||||||
|
|
||||||
|
|
||||||
|
def __init__(self, item):
|
||||||
|
|
||||||
|
self.item = item
|
||||||
|
self.API = api.API(self.item)
|
||||||
|
|
||||||
|
self.clientInfo = clientinfo.ClientInfo()
|
||||||
|
self.addonName = self.clientInfo.getAddonName()
|
||||||
|
self.doUtils = downloadutils.DownloadUtils()
|
||||||
|
|
||||||
|
self.userid = utils.window('emby_currUser')
|
||||||
|
self.server = utils.window('emby_server%s' % self.userid)
|
||||||
|
|
||||||
|
self.artwork = artwork.Artwork()
|
||||||
|
self.emby = embyserver.Read_EmbyServer()
|
||||||
|
self.pl = playlist.Playlist()
|
||||||
|
|
||||||
|
def logMsg(self, msg, lvl=1):
|
||||||
|
|
||||||
|
self.className = self.__class__.__name__
|
||||||
|
utils.logMsg("%s %s" % (self.addonName, self.className), msg, lvl)
|
||||||
|
|
||||||
|
|
||||||
|
def play(self, itemid, dbid=None):
|
||||||
|
|
||||||
|
self.logMsg("Play called.", 1)
|
||||||
|
|
||||||
|
doUtils = self.doUtils
|
||||||
|
item = self.item
|
||||||
|
API = self.API
|
||||||
|
listitem = xbmcgui.ListItem()
|
||||||
|
playutils = putils.PlayUtils(item)
|
||||||
|
|
||||||
|
playurl = playutils.getPlayUrl()
|
||||||
|
if not playurl:
|
||||||
|
return xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, listitem)
|
||||||
|
|
||||||
|
if dbid is None:
|
||||||
|
# Item is not in Kodi database
|
||||||
|
listitem.setPath(playurl)
|
||||||
|
self.setProperties(playurl, listitem)
|
||||||
|
return xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem)
|
||||||
|
|
||||||
|
############### ORGANIZE CURRENT PLAYLIST ################
|
||||||
|
|
||||||
|
homeScreen = xbmc.getCondVisibility('Window.IsActive(home)')
|
||||||
|
playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
|
||||||
|
startPos = max(playlist.getposition(), 0) # Can return -1
|
||||||
|
sizePlaylist = playlist.size()
|
||||||
|
currentPosition = startPos
|
||||||
|
|
||||||
|
propertiesPlayback = utils.window('emby_playbackProps', windowid=10101) == "true"
|
||||||
|
introsPlaylist = False
|
||||||
|
dummyPlaylist = False
|
||||||
|
|
||||||
|
self.logMsg("Playlist start position: %s" % startPos, 1)
|
||||||
|
self.logMsg("Playlist plugin position: %s" % currentPosition, 1)
|
||||||
|
self.logMsg("Playlist size: %s" % sizePlaylist, 1)
|
||||||
|
|
||||||
|
############### RESUME POINT ################
|
||||||
|
|
||||||
|
userdata = API.getUserData()
|
||||||
|
seektime = API.adjustResume(userdata['Resume'])
|
||||||
|
|
||||||
|
# We need to ensure we add the intro and additional parts only once.
|
||||||
|
# Otherwise we get a loop.
|
||||||
|
if not propertiesPlayback:
|
||||||
|
|
||||||
|
utils.window('emby_playbackProps', value="true", windowid=10101)
|
||||||
|
self.logMsg("Setting up properties in playlist.", 1)
|
||||||
|
|
||||||
|
if (not homeScreen and not seektime and
|
||||||
|
utils.window('emby_customPlaylist', windowid=10101) != "true"):
|
||||||
|
|
||||||
|
self.logMsg("Adding dummy file to playlist.", 2)
|
||||||
|
dummyPlaylist = True
|
||||||
|
playlist.add(playurl, listitem, index=startPos)
|
||||||
|
# Remove the original item from playlist
|
||||||
|
self.pl.removefromPlaylist(startPos+1)
|
||||||
|
# Readd the original item to playlist - via jsonrpc so we have full metadata
|
||||||
|
self.pl.insertintoPlaylist(currentPosition+1, dbid, item['Type'].lower())
|
||||||
|
currentPosition += 1
|
||||||
|
|
||||||
|
############### -- CHECK FOR INTROS ################
|
||||||
|
|
||||||
|
if utils.settings('enableCinema') == "true" and not seektime:
|
||||||
|
# if we have any play them when the movie/show is not being resumed
|
||||||
|
url = "{server}/emby/Users/{UserId}/Items/%s/Intros?format=json" % itemid
|
||||||
|
intros = doUtils.downloadUrl(url)
|
||||||
|
|
||||||
|
if intros['TotalRecordCount'] != 0:
|
||||||
|
getTrailers = True
|
||||||
|
|
||||||
|
if utils.settings('askCinema') == "true":
|
||||||
|
resp = xbmcgui.Dialog().yesno("Emby Cinema Mode", "Play trailers?")
|
||||||
|
if not resp:
|
||||||
|
# User selected to not play trailers
|
||||||
|
getTrailers = False
|
||||||
|
self.logMsg("Skip trailers.", 1)
|
||||||
|
|
||||||
|
if getTrailers:
|
||||||
|
for intro in intros['Items']:
|
||||||
|
# The server randomly returns intros, process them.
|
||||||
|
introListItem = xbmcgui.ListItem()
|
||||||
|
introPlayurl = putils.PlayUtils(intro).getPlayUrl()
|
||||||
|
self.logMsg("Adding Intro: %s" % introPlayurl, 1)
|
||||||
|
|
||||||
|
# Set listitem and properties for intros
|
||||||
|
pbutils = PlaybackUtils(intro)
|
||||||
|
pbutils.setProperties(introPlayurl, introListItem)
|
||||||
|
|
||||||
|
self.pl.insertintoPlaylist(currentPosition, url=introPlayurl)
|
||||||
|
introsPlaylist = True
|
||||||
|
currentPosition += 1
|
||||||
|
|
||||||
|
|
||||||
|
############### -- ADD MAIN ITEM ONLY FOR HOMESCREEN ###############
|
||||||
|
|
||||||
|
if homeScreen and not sizePlaylist:
|
||||||
|
# Extend our current playlist with the actual item to play
|
||||||
|
# only if there's no playlist first
|
||||||
|
self.logMsg("Adding main item to playlist.", 1)
|
||||||
|
self.pl.addtoPlaylist(dbid, item['Type'].lower())
|
||||||
|
|
||||||
|
# Ensure that additional parts are played after the main item
|
||||||
|
currentPosition += 1
|
||||||
|
|
||||||
|
############### -- CHECK FOR ADDITIONAL PARTS ################
|
||||||
|
|
||||||
|
if item.get('PartCount'):
|
||||||
|
# Only add to the playlist after intros have played
|
||||||
|
partcount = item['PartCount']
|
||||||
|
url = "{server}/emby/Videos/%s/AdditionalParts?format=json" % itemid
|
||||||
|
parts = doUtils.downloadUrl(url)
|
||||||
|
for part in parts['Items']:
|
||||||
|
|
||||||
|
additionalListItem = xbmcgui.ListItem()
|
||||||
|
additionalPlayurl = putils.PlayUtils(part).getPlayUrl()
|
||||||
|
self.logMsg("Adding additional part: %s" % partcount, 1)
|
||||||
|
|
||||||
|
# Set listitem and properties for each additional parts
|
||||||
|
pbutils = PlaybackUtils(part)
|
||||||
|
pbutils.setProperties(additionalPlayurl, additionalListItem)
|
||||||
|
pbutils.setArtwork(additionalListItem)
|
||||||
|
|
||||||
|
playlist.add(additionalPlayurl, additionalListItem, index=currentPosition)
|
||||||
|
self.pl.verifyPlaylist()
|
||||||
|
currentPosition += 1
|
||||||
|
|
||||||
|
if dummyPlaylist:
|
||||||
|
# Added a dummy file to the playlist,
|
||||||
|
# because the first item is going to fail automatically.
|
||||||
|
self.logMsg("Processed as a playlist. First item is skipped.", 1)
|
||||||
|
return xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, listitem)
|
||||||
|
|
||||||
|
|
||||||
|
# We just skipped adding properties. Reset flag for next time.
|
||||||
|
elif propertiesPlayback:
|
||||||
|
self.logMsg("Resetting properties playback flag.", 2)
|
||||||
|
utils.window('emby_playbackProps', clear=True, windowid=10101)
|
||||||
|
|
||||||
|
#self.pl.verifyPlaylist()
|
||||||
|
########## SETUP MAIN ITEM ##########
|
||||||
|
|
||||||
|
# For transcoding only, ask for audio/subs pref
|
||||||
|
if utils.window('emby_%s.playmethod' % playurl) == "Transcode":
|
||||||
|
playurl = playutils.audioSubsPref(playurl)
|
||||||
|
utils.window('emby_%s.playmethod' % playurl, value="Transcode")
|
||||||
|
|
||||||
|
listitem.setPath(playurl)
|
||||||
|
self.setProperties(playurl, listitem)
|
||||||
|
|
||||||
|
############### PLAYBACK ################
|
||||||
|
|
||||||
|
if homeScreen and seektime:
|
||||||
|
self.logMsg("Play as a widget item.", 1)
|
||||||
|
self.setListItem(listitem)
|
||||||
|
xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem)
|
||||||
|
|
||||||
|
elif ((introsPlaylist and utils.window('emby_customPlaylist', windowid=10101) == "true") or
|
||||||
|
(homeScreen and not sizePlaylist)):
|
||||||
|
# Playlist was created just now, play it.
|
||||||
|
self.logMsg("Play playlist.", 1)
|
||||||
|
xbmc.Player().play(playlist, startpos=startPos)
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.logMsg("Play as a regular item.", 1)
|
||||||
|
xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem)
|
||||||
|
|
||||||
|
def setProperties(self, playurl, listitem):
|
||||||
|
# Set all properties necessary for plugin path playback
|
||||||
|
item = self.item
|
||||||
|
itemid = item['Id']
|
||||||
|
itemtype = item['Type']
|
||||||
|
|
||||||
|
embyitem = "emby_%s" % playurl
|
||||||
|
utils.window('%s.runtime' % embyitem, value=str(item.get('RunTimeTicks')))
|
||||||
|
utils.window('%s.type' % embyitem, value=itemtype)
|
||||||
|
utils.window('%s.itemid' % embyitem, value=itemid)
|
||||||
|
|
||||||
|
if itemtype == "Episode":
|
||||||
|
utils.window('%s.refreshid' % embyitem, value=item.get('SeriesId'))
|
||||||
|
else:
|
||||||
|
utils.window('%s.refreshid' % embyitem, value=itemid)
|
||||||
|
|
||||||
|
# Append external subtitles to stream
|
||||||
|
playmethod = utils.window('%s.playmethod' % embyitem)
|
||||||
|
# Only for direct play and direct stream
|
||||||
|
subtitles = self.externalSubs(playurl)
|
||||||
|
if playmethod in ("DirectStream", "Transcode"):
|
||||||
|
# Direct play automatically appends external
|
||||||
|
listitem.setSubtitles(subtitles)
|
||||||
|
|
||||||
|
self.setArtwork(listitem)
|
||||||
|
|
||||||
|
def externalSubs(self, playurl):
|
||||||
|
|
||||||
|
externalsubs = []
|
||||||
|
mapping = {}
|
||||||
|
|
||||||
|
item = self.item
|
||||||
|
itemid = item['Id']
|
||||||
|
try:
|
||||||
|
mediastreams = item['MediaSources'][0]['MediaStreams']
|
||||||
|
except (TypeError, KeyError, IndexError):
|
||||||
|
return
|
||||||
|
|
||||||
|
kodiindex = 0
|
||||||
|
for stream in mediastreams:
|
||||||
|
|
||||||
|
index = stream['Index']
|
||||||
|
# Since Emby returns all possible tracks together, have to pull only external subtitles.
|
||||||
|
# IsTextSubtitleStream if true, is available to download from emby.
|
||||||
|
if (stream['Type'] == "Subtitle" and
|
||||||
|
stream['IsExternal'] and stream['IsTextSubtitleStream']):
|
||||||
|
|
||||||
|
# Direct stream
|
||||||
|
url = ("%s/Videos/%s/%s/Subtitles/%s/Stream.srt"
|
||||||
|
% (self.server, itemid, itemid, index))
|
||||||
|
|
||||||
|
# map external subtitles for mapping
|
||||||
|
mapping[kodiindex] = index
|
||||||
|
externalsubs.append(url)
|
||||||
|
kodiindex += 1
|
||||||
|
|
||||||
|
mapping = json.dumps(mapping)
|
||||||
|
utils.window('emby_%s.indexMapping' % playurl, value=mapping)
|
||||||
|
|
||||||
|
return externalsubs
|
||||||
|
|
||||||
|
def setArtwork(self, listItem):
|
||||||
|
# Set up item and item info
|
||||||
|
item = self.item
|
||||||
|
artwork = self.artwork
|
||||||
|
|
||||||
|
allartwork = artwork.getAllArtwork(item, parentInfo=True)
|
||||||
|
# Set artwork for listitem
|
||||||
|
arttypes = {
|
||||||
|
|
||||||
|
'poster': "Primary",
|
||||||
|
'tvshow.poster': "Primary",
|
||||||
|
'clearart': "Art",
|
||||||
|
'tvshow.clearart': "Art",
|
||||||
|
'clearlogo': "Logo",
|
||||||
|
'tvshow.clearlogo': "Logo",
|
||||||
|
'discart': "Disc",
|
||||||
|
'fanart_image': "Backdrop",
|
||||||
|
'landscape': "Thumb"
|
||||||
|
}
|
||||||
|
for arttype in arttypes:
|
||||||
|
|
||||||
|
art = arttypes[arttype]
|
||||||
|
if art == "Backdrop":
|
||||||
|
try: # Backdrop is a list, grab the first backdrop
|
||||||
|
self.setArtProp(listItem, arttype, allartwork[art][0])
|
||||||
|
except: pass
|
||||||
|
else:
|
||||||
|
self.setArtProp(listItem, arttype, allartwork[art])
|
||||||
|
|
||||||
|
def setArtProp(self, listItem, arttype, path):
|
||||||
|
|
||||||
|
if arttype in (
|
||||||
|
'thumb', 'fanart_image', 'small_poster', 'tiny_poster',
|
||||||
|
'medium_landscape', 'medium_poster', 'small_fanartimage',
|
||||||
|
'medium_fanartimage', 'fanart_noindicators'):
|
||||||
|
|
||||||
|
listItem.setProperty(arttype, path)
|
||||||
|
else:
|
||||||
|
listItem.setArt({arttype: path})
|
||||||
|
|
||||||
|
def setListItem(self, listItem):
|
||||||
|
|
||||||
|
item = self.item
|
||||||
|
type = item['Type']
|
||||||
|
API = self.API
|
||||||
|
people = API.getPeople()
|
||||||
|
studios = API.getStudios()
|
||||||
|
|
||||||
|
metadata = {
|
||||||
|
|
||||||
|
'title': item.get('Name', "Missing name"),
|
||||||
|
'year': item.get('ProductionYear'),
|
||||||
|
'plot': API.getOverview(),
|
||||||
|
'director': people.get('Director'),
|
||||||
|
'writer': people.get('Writer'),
|
||||||
|
'mpaa': API.getMpaa(),
|
||||||
|
'genre': " / ".join(item['Genres']),
|
||||||
|
'studio': " / ".join(studios),
|
||||||
|
'aired': API.getPremiereDate(),
|
||||||
|
'rating': item.get('CommunityRating'),
|
||||||
|
'votes': item.get('VoteCount')
|
||||||
|
}
|
||||||
|
|
||||||
|
if "Episode" in type:
|
||||||
|
# Only for tv shows
|
||||||
|
thumbId = item.get('SeriesId')
|
||||||
|
season = item.get('ParentIndexNumber', -1)
|
||||||
|
episode = item.get('IndexNumber', -1)
|
||||||
|
show = item.get('SeriesName', "")
|
||||||
|
|
||||||
|
metadata['TVShowTitle'] = show
|
||||||
|
metadata['season'] = season
|
||||||
|
metadata['episode'] = episode
|
||||||
|
|
||||||
|
listItem.setProperty('IsPlayable', 'true')
|
||||||
|
listItem.setProperty('IsFolder', 'false')
|
||||||
|
listItem.setLabel(metadata['title'])
|
||||||
|
listItem.setInfo('video', infoLabels=metadata)
|
502
resources/lib/player.py
Normal file
502
resources/lib/player.py
Normal file
|
@ -0,0 +1,502 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
#################################################################################################
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
import xbmc
|
||||||
|
import xbmcgui
|
||||||
|
|
||||||
|
import utils
|
||||||
|
import clientinfo
|
||||||
|
import downloadutils
|
||||||
|
import kodidb_functions as kodidb
|
||||||
|
import websocket_client as wsc
|
||||||
|
|
||||||
|
#################################################################################################
|
||||||
|
|
||||||
|
|
||||||
|
class Player(xbmc.Player):
|
||||||
|
|
||||||
|
# Borg - multiple instances, shared state
|
||||||
|
_shared_state = {}
|
||||||
|
|
||||||
|
played_info = {}
|
||||||
|
playStats = {}
|
||||||
|
currentFile = None
|
||||||
|
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
|
||||||
|
self.__dict__ = self._shared_state
|
||||||
|
|
||||||
|
self.clientInfo = clientinfo.ClientInfo()
|
||||||
|
self.addonName = self.clientInfo.getAddonName()
|
||||||
|
self.doUtils = downloadutils.DownloadUtils()
|
||||||
|
self.ws = wsc.WebSocket_Client()
|
||||||
|
self.xbmcplayer = xbmc.Player()
|
||||||
|
|
||||||
|
self.logMsg("Starting playback monitor.", 2)
|
||||||
|
|
||||||
|
def logMsg(self, msg, lvl=1):
|
||||||
|
|
||||||
|
self.className = self.__class__.__name__
|
||||||
|
utils.logMsg("%s %s" % (self.addonName, self.className), msg, lvl)
|
||||||
|
|
||||||
|
|
||||||
|
def GetPlayStats(self):
|
||||||
|
return self.playStats
|
||||||
|
|
||||||
|
def onPlayBackStarted( self ):
|
||||||
|
# Will be called when xbmc starts playing a file
|
||||||
|
xbmcplayer = self.xbmcplayer
|
||||||
|
self.stopAll()
|
||||||
|
|
||||||
|
# Get current file
|
||||||
|
try:
|
||||||
|
currentFile = xbmcplayer.getPlayingFile()
|
||||||
|
xbmc.sleep(300)
|
||||||
|
except:
|
||||||
|
currentFile = ""
|
||||||
|
count = 0
|
||||||
|
while not currentFile:
|
||||||
|
xbmc.sleep(100)
|
||||||
|
try:
|
||||||
|
currentFile = xbmcplayer.getPlayingFile()
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
if count == 5: # try 5 times
|
||||||
|
self.logMsg("Cancelling playback report...", 1)
|
||||||
|
break
|
||||||
|
else: count += 1
|
||||||
|
|
||||||
|
|
||||||
|
if currentFile:
|
||||||
|
|
||||||
|
self.currentFile = currentFile
|
||||||
|
|
||||||
|
# We may need to wait for info to be set in kodi monitor
|
||||||
|
itemId = utils.window("emby_%s.itemid" % currentFile)
|
||||||
|
tryCount = 0
|
||||||
|
while not itemId:
|
||||||
|
|
||||||
|
xbmc.sleep(200)
|
||||||
|
itemId = utils.window("emby_%s.itemid" % currentFile)
|
||||||
|
if tryCount == 20: # try 20 times or about 10 seconds
|
||||||
|
self.logMsg("Could not find itemId, cancelling playback report...", 1)
|
||||||
|
break
|
||||||
|
else: tryCount += 1
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.logMsg("ONPLAYBACK_STARTED: %s itemid: %s" % (currentFile, itemId), 0)
|
||||||
|
|
||||||
|
# Only proceed if an itemId was found.
|
||||||
|
embyitem = "emby_%s" % currentFile
|
||||||
|
runtime = utils.window("%s.runtime" % embyitem)
|
||||||
|
refresh_id = utils.window("%s.refreshid" % embyitem)
|
||||||
|
playMethod = utils.window("%s.playmethod" % embyitem)
|
||||||
|
itemType = utils.window("%s.type" % embyitem)
|
||||||
|
utils.window('emby_skipWatched%s' % itemId, value="true")
|
||||||
|
|
||||||
|
seekTime = xbmcplayer.getTime()
|
||||||
|
|
||||||
|
# Get playback volume
|
||||||
|
volume_query = {
|
||||||
|
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": 1,
|
||||||
|
"method": "Application.GetProperties",
|
||||||
|
"params": {
|
||||||
|
|
||||||
|
"properties": ["volume", "muted"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = xbmc.executeJSONRPC(json.dumps(volume_query))
|
||||||
|
result = json.loads(result)
|
||||||
|
result = result.get('result')
|
||||||
|
|
||||||
|
volume = result.get('volume')
|
||||||
|
muted = result.get('muted')
|
||||||
|
|
||||||
|
# Postdata structure to send to Emby server
|
||||||
|
url = "{server}/emby/Sessions/Playing"
|
||||||
|
postdata = {
|
||||||
|
|
||||||
|
'QueueableMediaTypes': "Video",
|
||||||
|
'CanSeek': True,
|
||||||
|
'ItemId': itemId,
|
||||||
|
'MediaSourceId': itemId,
|
||||||
|
'PlayMethod': playMethod,
|
||||||
|
'VolumeLevel': volume,
|
||||||
|
'PositionTicks': int(seekTime * 10000000),
|
||||||
|
'IsMuted': muted
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get the current audio track and subtitles
|
||||||
|
if playMethod == "Transcode":
|
||||||
|
# property set in PlayUtils.py
|
||||||
|
postdata['AudioStreamIndex'] = utils.window("%sAudioStreamIndex" % currentFile)
|
||||||
|
postdata['SubtitleStreamIndex'] = utils.window("%sSubtitleStreamIndex"
|
||||||
|
% currentFile)
|
||||||
|
else:
|
||||||
|
# Get the current kodi audio and subtitles and convert to Emby equivalent
|
||||||
|
tracks_query = {
|
||||||
|
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": 1,
|
||||||
|
"method": "Player.GetProperties",
|
||||||
|
"params": {
|
||||||
|
|
||||||
|
"playerid": 1,
|
||||||
|
"properties": ["currentsubtitle","currentaudiostream","subtitleenabled"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = xbmc.executeJSONRPC(json.dumps(tracks_query))
|
||||||
|
result = json.loads(result)
|
||||||
|
result = result.get('result')
|
||||||
|
|
||||||
|
try: # Audio tracks
|
||||||
|
indexAudio = result['currentaudiostream']['index']
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
indexAudio = 0
|
||||||
|
|
||||||
|
try: # Subtitles tracks
|
||||||
|
indexSubs = result['currentsubtitle']['index']
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
indexSubs = 0
|
||||||
|
|
||||||
|
try: # If subtitles are enabled
|
||||||
|
subsEnabled = result['subtitleenabled']
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
subsEnabled = ""
|
||||||
|
|
||||||
|
# Postdata for the audio
|
||||||
|
postdata['AudioStreamIndex'] = indexAudio + 1
|
||||||
|
|
||||||
|
# Postdata for the subtitles
|
||||||
|
if subsEnabled and len(xbmc.Player().getAvailableSubtitleStreams()) > 0:
|
||||||
|
|
||||||
|
# Number of audiotracks to help get Emby Index
|
||||||
|
audioTracks = len(xbmc.Player().getAvailableAudioStreams())
|
||||||
|
mapping = utils.window("%s.indexMapping" % embyitem)
|
||||||
|
|
||||||
|
if mapping: # Set in playbackutils.py
|
||||||
|
|
||||||
|
self.logMsg("Mapping for external subtitles index: %s" % mapping, 2)
|
||||||
|
externalIndex = json.loads(mapping)
|
||||||
|
|
||||||
|
if externalIndex.get(str(indexSubs)):
|
||||||
|
# If the current subtitle is in the mapping
|
||||||
|
postdata['SubtitleStreamIndex'] = externalIndex[str(indexSubs)]
|
||||||
|
else:
|
||||||
|
# Internal subtitle currently selected
|
||||||
|
subindex = indexSubs - len(externalIndex) + audioTracks + 1
|
||||||
|
postdata['SubtitleStreamIndex'] = subindex
|
||||||
|
|
||||||
|
else: # Direct paths enabled scenario or no external subtitles set
|
||||||
|
postdata['SubtitleStreamIndex'] = indexSubs + audioTracks + 1
|
||||||
|
else:
|
||||||
|
postdata['SubtitleStreamIndex'] = ""
|
||||||
|
|
||||||
|
|
||||||
|
# Post playback to server
|
||||||
|
self.logMsg("Sending POST play started: %s." % postdata, 2)
|
||||||
|
self.doUtils.downloadUrl(url, postBody=postdata, type="POST")
|
||||||
|
|
||||||
|
# Ensure we do have a runtime
|
||||||
|
try:
|
||||||
|
runtime = int(runtime)
|
||||||
|
except ValueError:
|
||||||
|
runtime = xbmcplayer.getTotalTime()
|
||||||
|
self.logMsg("Runtime is missing, Kodi runtime: %s" % runtime, 1)
|
||||||
|
|
||||||
|
# Save data map for updates and position calls
|
||||||
|
data = {
|
||||||
|
|
||||||
|
'runtime': runtime,
|
||||||
|
'item_id': itemId,
|
||||||
|
'refresh_id': refresh_id,
|
||||||
|
'currentfile': currentFile,
|
||||||
|
'AudioStreamIndex': postdata['AudioStreamIndex'],
|
||||||
|
'SubtitleStreamIndex': postdata['SubtitleStreamIndex'],
|
||||||
|
'playmethod': playMethod,
|
||||||
|
'Type': itemType,
|
||||||
|
'currentPosition': int(seekTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.played_info[currentFile] = data
|
||||||
|
self.logMsg("ADDING_FILE: %s" % self.played_info, 1)
|
||||||
|
|
||||||
|
# log some playback stats
|
||||||
|
'''if(itemType != None):
|
||||||
|
if(self.playStats.get(itemType) != None):
|
||||||
|
count = self.playStats.get(itemType) + 1
|
||||||
|
self.playStats[itemType] = count
|
||||||
|
else:
|
||||||
|
self.playStats[itemType] = 1
|
||||||
|
|
||||||
|
if(playMethod != None):
|
||||||
|
if(self.playStats.get(playMethod) != None):
|
||||||
|
count = self.playStats.get(playMethod) + 1
|
||||||
|
self.playStats[playMethod] = count
|
||||||
|
else:
|
||||||
|
self.playStats[playMethod] = 1'''
|
||||||
|
|
||||||
|
def reportPlayback(self):
|
||||||
|
|
||||||
|
self.logMsg("reportPlayback Called", 2)
|
||||||
|
xbmcplayer = self.xbmcplayer
|
||||||
|
|
||||||
|
# Get current file
|
||||||
|
currentFile = self.currentFile
|
||||||
|
data = self.played_info.get(currentFile)
|
||||||
|
|
||||||
|
# only report playback if emby has initiated the playback (item_id has value)
|
||||||
|
if data:
|
||||||
|
# Get playback information
|
||||||
|
itemId = data['item_id']
|
||||||
|
audioindex = data['AudioStreamIndex']
|
||||||
|
subtitleindex = data['SubtitleStreamIndex']
|
||||||
|
playTime = data['currentPosition']
|
||||||
|
playMethod = data['playmethod']
|
||||||
|
paused = data.get('paused', False)
|
||||||
|
|
||||||
|
|
||||||
|
# Get playback volume
|
||||||
|
volume_query = {
|
||||||
|
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": 1,
|
||||||
|
"method": "Application.GetProperties",
|
||||||
|
"params": {
|
||||||
|
|
||||||
|
"properties": ["volume", "muted"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = xbmc.executeJSONRPC(json.dumps(volume_query))
|
||||||
|
result = json.loads(result)
|
||||||
|
result = result.get('result')
|
||||||
|
|
||||||
|
volume = result.get('volume')
|
||||||
|
muted = result.get('muted')
|
||||||
|
|
||||||
|
# Postdata for the websocketclient report
|
||||||
|
postdata = {
|
||||||
|
|
||||||
|
'QueueableMediaTypes': "Video",
|
||||||
|
'CanSeek': True,
|
||||||
|
'ItemId': itemId,
|
||||||
|
'MediaSourceId': itemId,
|
||||||
|
'PlayMethod': playMethod,
|
||||||
|
'PositionTicks': int(playTime * 10000000),
|
||||||
|
'IsPaused': paused,
|
||||||
|
'VolumeLevel': volume,
|
||||||
|
'IsMuted': muted
|
||||||
|
}
|
||||||
|
|
||||||
|
if playMethod == "Transcode":
|
||||||
|
# Track can't be changed, keep reporting the same index
|
||||||
|
postdata['AudioStreamIndex'] = audioindex
|
||||||
|
postdata['AudioStreamIndex'] = subtitleindex
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Get current audio and subtitles track
|
||||||
|
tracks_query = {
|
||||||
|
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": 1,
|
||||||
|
"method": "Player.GetProperties",
|
||||||
|
"params": {
|
||||||
|
|
||||||
|
"playerid": 1,
|
||||||
|
"properties": ["currentsubtitle","currentaudiostream","subtitleenabled"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = xbmc.executeJSONRPC(json.dumps(tracks_query))
|
||||||
|
result = json.loads(result)
|
||||||
|
result = result.get('result')
|
||||||
|
|
||||||
|
try: # Audio tracks
|
||||||
|
indexAudio = result['currentaudiostream']['index']
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
indexAudio = 0
|
||||||
|
|
||||||
|
try: # Subtitles tracks
|
||||||
|
indexSubs = result['currentsubtitle']['index']
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
indexSubs = 0
|
||||||
|
|
||||||
|
try: # If subtitles are enabled
|
||||||
|
subsEnabled = result['subtitleenabled']
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
subsEnabled = ""
|
||||||
|
|
||||||
|
# Postdata for the audio
|
||||||
|
data['AudioStreamIndex'], postdata['AudioStreamIndex'] = [indexAudio + 1] * 2
|
||||||
|
|
||||||
|
# Postdata for the subtitles
|
||||||
|
if subsEnabled and len(xbmc.Player().getAvailableSubtitleStreams()) > 0:
|
||||||
|
|
||||||
|
# Number of audiotracks to help get Emby Index
|
||||||
|
audioTracks = len(xbmc.Player().getAvailableAudioStreams())
|
||||||
|
mapping = utils.window("emby_%s.indexMapping" % currentFile)
|
||||||
|
|
||||||
|
if mapping: # Set in PlaybackUtils.py
|
||||||
|
|
||||||
|
self.logMsg("Mapping for external subtitles index: %s" % mapping, 2)
|
||||||
|
externalIndex = json.loads(mapping)
|
||||||
|
|
||||||
|
if externalIndex.get(str(indexSubs)):
|
||||||
|
# If the current subtitle is in the mapping
|
||||||
|
subindex = [externalIndex[str(indexSubs)]] * 2
|
||||||
|
data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = subindex
|
||||||
|
else:
|
||||||
|
# Internal subtitle currently selected
|
||||||
|
subindex = [indexSubs - len(externalIndex) + audioTracks + 1] * 2
|
||||||
|
data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = subindex
|
||||||
|
|
||||||
|
else: # Direct paths enabled scenario or no external subtitles set
|
||||||
|
subindex = [indexSubs + audioTracks + 1] * 2
|
||||||
|
data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = subindex
|
||||||
|
else:
|
||||||
|
data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = [""] * 2
|
||||||
|
|
||||||
|
# Report progress via websocketclient
|
||||||
|
postdata = json.dumps(postdata)
|
||||||
|
self.logMsg("Report: %s" % postdata, 2)
|
||||||
|
self.ws.sendProgressUpdate(postdata)
|
||||||
|
|
||||||
|
def onPlayBackPaused( self ):
|
||||||
|
|
||||||
|
currentFile = self.currentFile
|
||||||
|
self.logMsg("PLAYBACK_PAUSED: %s" % currentFile, 2)
|
||||||
|
|
||||||
|
if self.played_info.get(currentFile):
|
||||||
|
self.played_info[currentFile]['paused'] = True
|
||||||
|
|
||||||
|
self.reportPlayback()
|
||||||
|
|
||||||
|
def onPlayBackResumed( self ):
|
||||||
|
|
||||||
|
currentFile = self.currentFile
|
||||||
|
self.logMsg("PLAYBACK_RESUMED: %s" % currentFile, 2)
|
||||||
|
|
||||||
|
if self.played_info.get(currentFile):
|
||||||
|
self.played_info[currentFile]['paused'] = False
|
||||||
|
|
||||||
|
self.reportPlayback()
|
||||||
|
|
||||||
|
def onPlayBackSeek( self, time, seekOffset ):
|
||||||
|
# Make position when seeking a bit more accurate
|
||||||
|
currentFile = self.currentFile
|
||||||
|
self.logMsg("PLAYBACK_SEEK: %s" % currentFile, 2)
|
||||||
|
|
||||||
|
if self.played_info.get(currentFile):
|
||||||
|
position = self.xbmcplayer.getTime()
|
||||||
|
self.played_info[currentFile]['currentPosition'] = position
|
||||||
|
|
||||||
|
self.reportPlayback()
|
||||||
|
|
||||||
|
def onPlayBackStopped( self ):
|
||||||
|
# Will be called when user stops xbmc playing a file
|
||||||
|
self.logMsg("ONPLAYBACK_STOPPED", 2)
|
||||||
|
xbmcgui.Window(10101).clearProperties()
|
||||||
|
self.logMsg("Clear playlist properties.")
|
||||||
|
self.stopAll()
|
||||||
|
|
||||||
|
def onPlayBackEnded( self ):
|
||||||
|
# Will be called when xbmc stops playing a file
|
||||||
|
self.logMsg("ONPLAYBACK_ENDED", 2)
|
||||||
|
self.stopAll()
|
||||||
|
|
||||||
|
def stopAll(self):
|
||||||
|
|
||||||
|
doUtils = self.doUtils
|
||||||
|
|
||||||
|
if not self.played_info:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.logMsg("Played_information: %s" % self.played_info, 1)
|
||||||
|
# Process each items
|
||||||
|
for item in self.played_info:
|
||||||
|
|
||||||
|
data = self.played_info.get(item)
|
||||||
|
if data:
|
||||||
|
|
||||||
|
self.logMsg("Item path: %s" % item, 2)
|
||||||
|
self.logMsg("Item data: %s" % data, 2)
|
||||||
|
|
||||||
|
runtime = data['runtime']
|
||||||
|
currentPosition = data['currentPosition']
|
||||||
|
itemid = data['item_id']
|
||||||
|
refresh_id = data['refresh_id']
|
||||||
|
currentFile = data['currentfile']
|
||||||
|
type = data['Type']
|
||||||
|
playMethod = data['playmethod']
|
||||||
|
|
||||||
|
if currentPosition and runtime:
|
||||||
|
try:
|
||||||
|
percentComplete = (currentPosition * 10000000) / int(runtime)
|
||||||
|
except ZeroDivisionError:
|
||||||
|
# Runtime is 0.
|
||||||
|
percentComplete = 0
|
||||||
|
|
||||||
|
markPlayedAt = float(utils.settings('markPlayed')) / 100
|
||||||
|
self.logMsg(
|
||||||
|
"Percent complete: %s Mark played at: %s"
|
||||||
|
% (percentComplete, markPlayedAt), 1)
|
||||||
|
|
||||||
|
# Prevent manually mark as watched in Kodi monitor
|
||||||
|
utils.window('emby_skipWatched%s' % itemid, value="true")
|
||||||
|
|
||||||
|
self.stopPlayback(data)
|
||||||
|
# Stop transcoding
|
||||||
|
if playMethod == "Transcode":
|
||||||
|
self.logMsg("Transcoding for %s terminated." % itemid, 1)
|
||||||
|
deviceId = self.clientInfo.getDeviceId()
|
||||||
|
url = "{server}/emby/Videos/ActiveEncodings?DeviceId=%s" % deviceId
|
||||||
|
doUtils.downloadUrl(url, type="DELETE")
|
||||||
|
|
||||||
|
# Send the delete action to the server.
|
||||||
|
offerDelete = False
|
||||||
|
|
||||||
|
if type == "Episode" and utils.settings('deleteTV') == "true":
|
||||||
|
offerDelete = True
|
||||||
|
elif type == "Movie" and utils.settings('deleteMovies') == "true":
|
||||||
|
offerDelete = True
|
||||||
|
|
||||||
|
if utils.settings('offerDelete') != "true":
|
||||||
|
# Delete could be disabled, even if the subsetting is enabled.
|
||||||
|
offerDelete = False
|
||||||
|
|
||||||
|
if percentComplete >= markPlayedAt and offerDelete:
|
||||||
|
if utils.settings('skipConfirmDelete') != "true":
|
||||||
|
resp = xbmcgui.Dialog().yesno(
|
||||||
|
heading="Confirm delete",
|
||||||
|
line1="Delete file on Emby Server?")
|
||||||
|
if not resp:
|
||||||
|
self.logMsg("User skipped deletion.", 1)
|
||||||
|
continue
|
||||||
|
|
||||||
|
url = "{server}/emby/Items/%s?format=json" % itemid
|
||||||
|
self.logMsg("Deleting request: %s" % itemid)
|
||||||
|
doUtils.downloadUrl(url, type="DELETE")
|
||||||
|
|
||||||
|
self.played_info.clear()
|
||||||
|
|
||||||
|
def stopPlayback(self, data):
|
||||||
|
|
||||||
|
self.logMsg("stopPlayback called", 2)
|
||||||
|
|
||||||
|
itemId = data['item_id']
|
||||||
|
currentPosition = data['currentPosition']
|
||||||
|
positionTicks = int(currentPosition * 10000000)
|
||||||
|
|
||||||
|
url = "{server}/emby/Sessions/Playing/Stopped"
|
||||||
|
postdata = {
|
||||||
|
|
||||||
|
'ItemId': itemId,
|
||||||
|
'MediaSourceId': itemId,
|
||||||
|
'PositionTicks': positionTicks
|
||||||
|
}
|
||||||
|
self.doUtils.downloadUrl(url, postBody=postdata, type="POST")
|
397
resources/lib/playutils.py
Normal file
397
resources/lib/playutils.py
Normal file
|
@ -0,0 +1,397 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
#################################################################################################
|
||||||
|
|
||||||
|
import xbmc
|
||||||
|
import xbmcgui
|
||||||
|
import xbmcvfs
|
||||||
|
|
||||||
|
import clientinfo
|
||||||
|
import utils
|
||||||
|
|
||||||
|
#################################################################################################
|
||||||
|
|
||||||
|
|
||||||
|
class PlayUtils():
|
||||||
|
|
||||||
|
|
||||||
|
def __init__(self, item):
|
||||||
|
|
||||||
|
self.item = item
|
||||||
|
|
||||||
|
self.clientInfo = clientinfo.ClientInfo()
|
||||||
|
self.addonName = self.clientInfo.getAddonName()
|
||||||
|
|
||||||
|
self.userid = utils.window('emby_currUser')
|
||||||
|
self.server = utils.window('emby_server%s' % self.userid)
|
||||||
|
|
||||||
|
def logMsg(self, msg, lvl=1):
|
||||||
|
|
||||||
|
self.className = self.__class__.__name__
|
||||||
|
utils.logMsg("%s %s" % (self.addonName, self.className), msg, lvl)
|
||||||
|
|
||||||
|
|
||||||
|
def getPlayUrl(self):
|
||||||
|
|
||||||
|
item = self.item
|
||||||
|
playurl = None
|
||||||
|
|
||||||
|
if item['MediaSources'][0]['Protocol'] == "Http":
|
||||||
|
# Only play as http
|
||||||
|
self.logMsg("File protocol is http.", 1)
|
||||||
|
playurl = self.httpPlay()
|
||||||
|
utils.window('emby_%s.playmethod' % playurl, value="DirectStream")
|
||||||
|
|
||||||
|
elif self.isDirectPlay():
|
||||||
|
|
||||||
|
self.logMsg("File is direct playing.", 1)
|
||||||
|
playurl = self.directPlay()
|
||||||
|
playurl = playurl.encode('utf-8')
|
||||||
|
# Set playmethod property
|
||||||
|
utils.window('emby_%s.playmethod' % playurl, value="DirectPlay")
|
||||||
|
|
||||||
|
elif self.isDirectStream():
|
||||||
|
|
||||||
|
self.logMsg("File is direct streaming.", 1)
|
||||||
|
playurl = self.directStream()
|
||||||
|
# Set playmethod property
|
||||||
|
utils.window('emby_%s.playmethod' % playurl, value="DirectStream")
|
||||||
|
|
||||||
|
elif self.isTranscoding():
|
||||||
|
|
||||||
|
self.logMsg("File is transcoding.", 1)
|
||||||
|
playurl = self.transcoding()
|
||||||
|
# Set playmethod property
|
||||||
|
utils.window('emby_%s.playmethod' % playurl, value="Transcode")
|
||||||
|
|
||||||
|
return playurl
|
||||||
|
|
||||||
|
def httpPlay(self):
|
||||||
|
# Audio, Video, Photo
|
||||||
|
item = self.item
|
||||||
|
server = self.server
|
||||||
|
|
||||||
|
itemid = item['Id']
|
||||||
|
mediatype = item['MediaType']
|
||||||
|
|
||||||
|
if type == "Audio":
|
||||||
|
playurl = "%s/emby/Audio/%s/stream" % (server, itemid)
|
||||||
|
else:
|
||||||
|
playurl = "%s/emby/Videos/%s/stream?static=true" % (server, itemid)
|
||||||
|
|
||||||
|
return playurl
|
||||||
|
|
||||||
|
def isDirectPlay(self):
|
||||||
|
|
||||||
|
item = self.item
|
||||||
|
|
||||||
|
# Requirement: Filesystem, Accessible path
|
||||||
|
if utils.settings('playFromStream') == "true":
|
||||||
|
# User forcing to play via HTTP
|
||||||
|
self.logMsg("Can't direct play, play from HTTP enabled.", 1)
|
||||||
|
return False
|
||||||
|
|
||||||
|
if (utils.settings('transcodeH265') == "true" and
|
||||||
|
result['MediaSources'][0]['Name'].startswith("1080P/H265")):
|
||||||
|
# Avoid H265 1080p
|
||||||
|
self.logMsg("Option to transcode 1080P/H265 enabled.", 1)
|
||||||
|
return False
|
||||||
|
|
||||||
|
canDirectPlay = item['MediaSources'][0]['SupportsDirectPlay']
|
||||||
|
# Make sure direct play is supported by the server
|
||||||
|
if not canDirectPlay:
|
||||||
|
self.logMsg("Can't direct play, server doesn't allow/support it.", 1)
|
||||||
|
return False
|
||||||
|
|
||||||
|
location = item['LocationType']
|
||||||
|
if location == "FileSystem":
|
||||||
|
# Verify the path
|
||||||
|
if not self.fileExists():
|
||||||
|
self.logMsg("Unable to direct play.")
|
||||||
|
try:
|
||||||
|
count = int(utils.settings('failCount'))
|
||||||
|
except ValueError:
|
||||||
|
count = 0
|
||||||
|
self.logMsg("Direct play failed: %s times." % count, 1)
|
||||||
|
|
||||||
|
if count < 2:
|
||||||
|
# Let the user know that direct play failed
|
||||||
|
utils.settings('failCount', value=str(count+1))
|
||||||
|
xbmcgui.Dialog().notification(
|
||||||
|
heading="Emby server",
|
||||||
|
message="Unable to direct play.",
|
||||||
|
icon="special://home/addons/plugin.video.emby/icon.png",
|
||||||
|
sound=False)
|
||||||
|
elif utils.settings('playFromStream') != "true":
|
||||||
|
# Permanently set direct stream as true
|
||||||
|
utils.settings('playFromStream', value="true")
|
||||||
|
utils.settings('failCount', value="0")
|
||||||
|
xbmcgui.Dialog().notification(
|
||||||
|
heading="Emby server",
|
||||||
|
message=("Direct play failed 3 times. Enabled play "
|
||||||
|
"from HTTP in the add-on settings."),
|
||||||
|
icon="special://home/addons/plugin.video.emby/icon.png",
|
||||||
|
sound=False)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def directPlay(self):
|
||||||
|
|
||||||
|
item = self.item
|
||||||
|
|
||||||
|
try:
|
||||||
|
playurl = item['MediaSources'][0]['Path']
|
||||||
|
except (IndexError, KeyError):
|
||||||
|
playurl = item['Path']
|
||||||
|
|
||||||
|
if item.get('VideoType'):
|
||||||
|
# Specific format modification
|
||||||
|
type = item['VideoType']
|
||||||
|
|
||||||
|
if type == "Dvd":
|
||||||
|
playurl = "%s/VIDEO_TS/VIDEO_TS.IFO" % playurl
|
||||||
|
elif type == "Bluray":
|
||||||
|
playurl = "%s/BDMV/index.bdmv" % playurl
|
||||||
|
|
||||||
|
# Assign network protocol
|
||||||
|
if playurl.startswith('\\\\'):
|
||||||
|
playurl = playurl.replace("\\\\", "smb://")
|
||||||
|
playurl = playurl.replace("\\", "/")
|
||||||
|
|
||||||
|
if "apple.com" in playurl:
|
||||||
|
USER_AGENT = "QuickTime/7.7.4"
|
||||||
|
playurl += "?|User-Agent=%s" % USER_AGENT
|
||||||
|
|
||||||
|
return playurl
|
||||||
|
|
||||||
|
def fileExists(self):
|
||||||
|
|
||||||
|
if 'Path' not in self.item:
|
||||||
|
# File has no path defined in server
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Convert path to direct play
|
||||||
|
path = self.directPlay()
|
||||||
|
self.logMsg("Verifying path: %s" % path, 1)
|
||||||
|
|
||||||
|
if xbmcvfs.exists(path):
|
||||||
|
self.logMsg("Path exists.", 1)
|
||||||
|
return True
|
||||||
|
|
||||||
|
elif ":" not in path:
|
||||||
|
self.logMsg("Can't verify path, assumed linux. Still try to direct play.", 1)
|
||||||
|
return True
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.logMsg("Failed to find file.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def isDirectStream(self):
|
||||||
|
|
||||||
|
item = self.item
|
||||||
|
|
||||||
|
if (utils.settings('transcodeH265') == "true" and
|
||||||
|
result['MediaSources'][0]['Name'].startswith("1080P/H265")):
|
||||||
|
# Avoid H265 1080p
|
||||||
|
self.logMsg("Option to transcode 1080P/H265 enabled.", 1)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Requirement: BitRate, supported encoding
|
||||||
|
canDirectStream = item['MediaSources'][0]['SupportsDirectStream']
|
||||||
|
# Make sure the server supports it
|
||||||
|
if not canDirectStream:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Verify the bitrate
|
||||||
|
if not self.isNetworkSufficient():
|
||||||
|
self.logMsg("The network speed is insufficient to direct stream file.", 1)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def directStream(self):
|
||||||
|
|
||||||
|
item = self.item
|
||||||
|
server = self.server
|
||||||
|
|
||||||
|
itemid = item['Id']
|
||||||
|
type = item['Type']
|
||||||
|
|
||||||
|
if 'Path' in item and item['Path'].endswith('.strm'):
|
||||||
|
# Allow strm loading when direct streaming
|
||||||
|
playurl = self.directPlay()
|
||||||
|
elif type == "Audio":
|
||||||
|
playurl = "%s/emby/Audio/%s/stream.mp3" % (server, itemid)
|
||||||
|
else:
|
||||||
|
playurl = "%s/emby/Videos/%s/stream?static=true" % (server, itemid)
|
||||||
|
|
||||||
|
return playurl
|
||||||
|
|
||||||
|
def isNetworkSufficient(self):
|
||||||
|
|
||||||
|
settings = self.getBitrate()*1000
|
||||||
|
|
||||||
|
try:
|
||||||
|
sourceBitrate = int(self.item['MediaSources'][0]['Bitrate'])
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
self.logMsg("Bitrate value is missing.", 1)
|
||||||
|
else:
|
||||||
|
self.logMsg("The add-on settings bitrate is: %s, the video bitrate required is: %s"
|
||||||
|
% (settings, sourceBitrate), 1)
|
||||||
|
if settings < sourceBitrate:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def isTranscoding(self):
|
||||||
|
|
||||||
|
item = self.item
|
||||||
|
|
||||||
|
canTranscode = item['MediaSources'][0]['SupportsTranscoding']
|
||||||
|
# Make sure the server supports it
|
||||||
|
if not canTranscode:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def transcoding(self):
|
||||||
|
|
||||||
|
item = self.item
|
||||||
|
|
||||||
|
if 'Path' in item and item['Path'].endswith('.strm'):
|
||||||
|
# Allow strm loading when transcoding
|
||||||
|
playurl = self.directPlay()
|
||||||
|
else:
|
||||||
|
itemid = item['Id']
|
||||||
|
deviceId = self.clientInfo.getDeviceId()
|
||||||
|
playurl = (
|
||||||
|
"%s/emby/Videos/%s/master.m3u8?MediaSourceId=%s"
|
||||||
|
% (self.server, itemid, itemid)
|
||||||
|
)
|
||||||
|
playurl = (
|
||||||
|
"%s&VideoCodec=h264&AudioCodec=ac3&MaxAudioChannels=6&deviceId=%s&VideoBitrate=%s"
|
||||||
|
% (playurl, deviceId, self.getBitrate()*1000))
|
||||||
|
|
||||||
|
return playurl
|
||||||
|
|
||||||
|
def getBitrate(self):
|
||||||
|
|
||||||
|
# get the addon video quality
|
||||||
|
videoQuality = utils.settings('videoBitrate')
|
||||||
|
bitrate = {
|
||||||
|
|
||||||
|
'0': 664,
|
||||||
|
'1': 996,
|
||||||
|
'2': 1320,
|
||||||
|
'3': 2000,
|
||||||
|
'4': 3200,
|
||||||
|
'5': 4700,
|
||||||
|
'6': 6200,
|
||||||
|
'7': 7700,
|
||||||
|
'8': 9200,
|
||||||
|
'9': 10700,
|
||||||
|
'10': 12200,
|
||||||
|
'11': 13700,
|
||||||
|
'12': 15200,
|
||||||
|
'13': 16700,
|
||||||
|
'14': 18200,
|
||||||
|
'15': 20000,
|
||||||
|
'16': 40000,
|
||||||
|
'17': 100000,
|
||||||
|
'18': 1000000
|
||||||
|
}
|
||||||
|
|
||||||
|
# max bit rate supported by server (max signed 32bit integer)
|
||||||
|
return bitrate.get(videoQuality, 2147483)
|
||||||
|
|
||||||
|
def audioSubsPref(self, url):
|
||||||
|
# For transcoding only
|
||||||
|
# Present the list of audio to select from
|
||||||
|
audioStreamsList = {}
|
||||||
|
audioStreams = []
|
||||||
|
audioStreamsChannelsList = {}
|
||||||
|
subtitleStreamsList = {}
|
||||||
|
subtitleStreams = ['No subtitles']
|
||||||
|
selectAudioIndex = ""
|
||||||
|
selectSubsIndex = ""
|
||||||
|
playurlprefs = "%s" % url
|
||||||
|
|
||||||
|
item = self.item
|
||||||
|
try:
|
||||||
|
mediasources = item['MediaSources'][0]
|
||||||
|
mediastreams = mediasources['MediaStreams']
|
||||||
|
except (TypeError, KeyError, IndexError):
|
||||||
|
return
|
||||||
|
|
||||||
|
for stream in mediastreams:
|
||||||
|
# Since Emby returns all possible tracks together, have to sort them.
|
||||||
|
index = stream['Index']
|
||||||
|
type = stream['Type']
|
||||||
|
|
||||||
|
if 'Audio' in type:
|
||||||
|
codec = stream['Codec']
|
||||||
|
channelLayout = stream.get('ChannelLayout', "")
|
||||||
|
|
||||||
|
try:
|
||||||
|
track = "%s - %s - %s %s" % (index, stream['Language'], codec, channelLayout)
|
||||||
|
except:
|
||||||
|
track = "%s - %s %s" % (index, codec, channelLayout)
|
||||||
|
|
||||||
|
audioStreamsChannelsList[index] = stream['Channels']
|
||||||
|
audioStreamsList[track] = index
|
||||||
|
audioStreams.append(track)
|
||||||
|
|
||||||
|
elif 'Subtitle' in type:
|
||||||
|
if stream['IsExternal']:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
track = "%s - %s" % (index, stream['Language'])
|
||||||
|
except:
|
||||||
|
track = "%s - %s" % (index, stream['Codec'])
|
||||||
|
|
||||||
|
default = stream['IsDefault']
|
||||||
|
forced = stream['IsForced']
|
||||||
|
if default:
|
||||||
|
track = "%s - Default" % track
|
||||||
|
if forced:
|
||||||
|
track = "%s - Forced" % track
|
||||||
|
|
||||||
|
subtitleStreamsList[track] = index
|
||||||
|
subtitleStreams.append(track)
|
||||||
|
|
||||||
|
|
||||||
|
if len(audioStreams) > 1:
|
||||||
|
resp = xbmcgui.Dialog().select("Choose the audio stream", audioStreams)
|
||||||
|
if resp > -1:
|
||||||
|
# User selected audio
|
||||||
|
selected = audioStreams[resp]
|
||||||
|
selectAudioIndex = audioStreamsList[selected]
|
||||||
|
playurlprefs += "&AudioStreamIndex=%s" % selectAudioIndex
|
||||||
|
else: # User backed out of selection
|
||||||
|
playurlprefs += "&AudioStreamIndex=%s" % mediasources['DefaultAudioStreamIndex']
|
||||||
|
else: # There's only one audiotrack.
|
||||||
|
selectAudioIndex = audioStreamsList[audioStreams[0]]
|
||||||
|
playurlprefs += "&AudioStreamIndex=%s" % selectAudioIndex
|
||||||
|
|
||||||
|
if len(subtitleStreams) > 1:
|
||||||
|
resp = xbmcgui.Dialog().select("Choose the subtitle stream", subtitleStreams)
|
||||||
|
if resp == 0:
|
||||||
|
# User selected no subtitles
|
||||||
|
pass
|
||||||
|
elif resp > -1:
|
||||||
|
# User selected subtitles
|
||||||
|
selected = subtitleStreams[resp]
|
||||||
|
selectSubsIndex = subtitleStreamsList[selected]
|
||||||
|
playurlprefs += "&SubtitleStreamIndex=%s" % selectSubsIndex
|
||||||
|
else: # User backed out of selection
|
||||||
|
playurlprefs += "&SubtitleStreamIndex=%s" % mediasources.get('DefaultSubtitleStreamIndex', "")
|
||||||
|
|
||||||
|
# Get number of channels for selected audio track
|
||||||
|
audioChannels = audioStreamsChannelsList.get(selectAudioIndex, 0)
|
||||||
|
if audioChannels > 2:
|
||||||
|
playurlprefs += "&AudioBitrate=384000"
|
||||||
|
else:
|
||||||
|
playurlprefs += "&AudioBitrate=192000"
|
||||||
|
|
||||||
|
return playurlprefs
|
471
resources/lib/userclient.py
Normal file
471
resources/lib/userclient.py
Normal file
|
@ -0,0 +1,471 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
##################################################################################################
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import threading
|
||||||
|
|
||||||
|
import xbmc
|
||||||
|
import xbmcgui
|
||||||
|
import xbmcaddon
|
||||||
|
import xbmcvfs
|
||||||
|
|
||||||
|
import artwork
|
||||||
|
import utils
|
||||||
|
import clientinfo
|
||||||
|
import downloadutils
|
||||||
|
|
||||||
|
##################################################################################################
|
||||||
|
|
||||||
|
|
||||||
|
class UserClient(threading.Thread):
|
||||||
|
|
||||||
|
# Borg - multiple instances, shared state
|
||||||
|
_shared_state = {}
|
||||||
|
|
||||||
|
stopClient = False
|
||||||
|
auth = True
|
||||||
|
retry = 0
|
||||||
|
|
||||||
|
currUser = None
|
||||||
|
currUserId = None
|
||||||
|
currServer = None
|
||||||
|
currToken = None
|
||||||
|
HasAccess = True
|
||||||
|
AdditionalUser = []
|
||||||
|
|
||||||
|
userSettings = None
|
||||||
|
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
|
||||||
|
self.__dict__ = self._shared_state
|
||||||
|
self.addon = xbmcaddon.Addon()
|
||||||
|
|
||||||
|
self.addonName = clientinfo.ClientInfo().getAddonName()
|
||||||
|
self.doUtils = downloadutils.DownloadUtils()
|
||||||
|
self.logLevel = int(utils.settings('logLevel'))
|
||||||
|
|
||||||
|
threading.Thread.__init__(self)
|
||||||
|
|
||||||
|
def logMsg(self, msg, lvl=1):
|
||||||
|
|
||||||
|
className = self.__class__.__name__
|
||||||
|
utils.logMsg("%s %s" % (self.addonName, className), msg, lvl)
|
||||||
|
|
||||||
|
|
||||||
|
def getAdditionalUsers(self):
|
||||||
|
|
||||||
|
additionalUsers = utils.settings('additionalUsers')
|
||||||
|
|
||||||
|
if additionalUsers:
|
||||||
|
self.AdditionalUser = additionalUsers.split(',')
|
||||||
|
|
||||||
|
def getUsername(self):
|
||||||
|
|
||||||
|
username = utils.settings('username')
|
||||||
|
|
||||||
|
if not username:
|
||||||
|
self.logMsg("No username saved.", 2)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
return username
|
||||||
|
|
||||||
|
def getLogLevel(self):
|
||||||
|
|
||||||
|
try:
|
||||||
|
logLevel = int(utils.settings('logLevel'))
|
||||||
|
except ValueError:
|
||||||
|
logLevel = 0
|
||||||
|
|
||||||
|
return logLevel
|
||||||
|
|
||||||
|
def getUserId(self):
|
||||||
|
|
||||||
|
username = self.getUsername()
|
||||||
|
w_userId = utils.window('emby_userId%s' % username)
|
||||||
|
s_userId = utils.settings('userId%s' % username)
|
||||||
|
|
||||||
|
# Verify the window property
|
||||||
|
if w_userId:
|
||||||
|
if not s_userId:
|
||||||
|
# Save access token if it's missing from settings
|
||||||
|
utils.settings('userId%s' % username, value=w_userId)
|
||||||
|
self.logMsg(
|
||||||
|
"Returning userId from WINDOW for username: %s UserId: %s"
|
||||||
|
% (username, w_userId), 2)
|
||||||
|
return w_userId
|
||||||
|
# Verify the settings
|
||||||
|
elif s_userId:
|
||||||
|
self.logMsg(
|
||||||
|
"Returning userId from SETTINGS for username: %s userId: %s"
|
||||||
|
% (username, s_userId), 2)
|
||||||
|
return s_userId
|
||||||
|
# No userId found
|
||||||
|
else:
|
||||||
|
self.logMsg("No userId saved for username: %s." % username, 1)
|
||||||
|
|
||||||
|
def getServer(self, prefix=True):
|
||||||
|
|
||||||
|
alternate = utils.settings('altip') == "true"
|
||||||
|
if alternate:
|
||||||
|
# Alternate host
|
||||||
|
HTTPS = utils.settings('secondhttps') == "true"
|
||||||
|
host = utils.settings('secondipaddress')
|
||||||
|
port = utils.settings('secondport')
|
||||||
|
else:
|
||||||
|
# Original host
|
||||||
|
HTTPS = utils.settings('https') == "true"
|
||||||
|
host = utils.settings('ipaddress')
|
||||||
|
port = utils.settings('port')
|
||||||
|
|
||||||
|
server = host + ":" + port
|
||||||
|
|
||||||
|
if not host:
|
||||||
|
self.logMsg("No server information saved.", 2)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# If https is true
|
||||||
|
if prefix and HTTPS:
|
||||||
|
server = "https://%s" % server
|
||||||
|
return server
|
||||||
|
# If https is false
|
||||||
|
elif prefix and not HTTPS:
|
||||||
|
server = "http://%s" % server
|
||||||
|
return server
|
||||||
|
# If only the host:port is required
|
||||||
|
elif not prefix:
|
||||||
|
return server
|
||||||
|
|
||||||
|
def getToken(self):
|
||||||
|
|
||||||
|
username = self.getUsername()
|
||||||
|
w_token = utils.window('emby_accessToken%s' % username)
|
||||||
|
s_token = utils.settings('accessToken')
|
||||||
|
|
||||||
|
# Verify the window property
|
||||||
|
if w_token:
|
||||||
|
if not s_token:
|
||||||
|
# Save access token if it's missing from settings
|
||||||
|
utils.settings('accessToken', value=w_token)
|
||||||
|
self.logMsg(
|
||||||
|
"Returning accessToken from WINDOW for username: %s accessToken: %s"
|
||||||
|
% (username, w_token), 2)
|
||||||
|
return w_token
|
||||||
|
# Verify the settings
|
||||||
|
elif s_token:
|
||||||
|
self.logMsg(
|
||||||
|
"Returning accessToken from SETTINGS for username: %s accessToken: %s"
|
||||||
|
% (username, s_token), 2)
|
||||||
|
utils.window('emby_accessToken%s' % username, value=s_token)
|
||||||
|
return s_token
|
||||||
|
else:
|
||||||
|
self.logMsg("No token found.", 1)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def getSSLverify(self):
|
||||||
|
# Verify host certificate
|
||||||
|
s_sslverify = utils.settings('sslverify')
|
||||||
|
if utils.settings('altip') == "true":
|
||||||
|
s_sslverify = utils.settings('secondsslverify')
|
||||||
|
|
||||||
|
if s_sslverify == "true":
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def getSSL(self):
|
||||||
|
# Client side certificate
|
||||||
|
s_cert = utils.settings('sslcert')
|
||||||
|
if utils.settings('altip') == "true":
|
||||||
|
s_cert = utils.settings('secondsslcert')
|
||||||
|
|
||||||
|
if s_cert == "None":
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return s_cert
|
||||||
|
|
||||||
|
def setUserPref(self):
|
||||||
|
|
||||||
|
doUtils = self.doUtils
|
||||||
|
|
||||||
|
url = "{server}/emby/Users/{UserId}?format=json"
|
||||||
|
result = doUtils.downloadUrl(url)
|
||||||
|
self.userSettings = result
|
||||||
|
# Set user image for skin display
|
||||||
|
if result.get('PrimaryImageTag'):
|
||||||
|
utils.window('EmbyUserImage', value=artwork.Artwork().getUserArtwork(result, 'Primary'))
|
||||||
|
|
||||||
|
# Set resume point max
|
||||||
|
url = "{server}/emby/System/Configuration?format=json"
|
||||||
|
result = doUtils.downloadUrl(url)
|
||||||
|
|
||||||
|
utils.settings('markPlayed', value=str(result['MaxResumePct']))
|
||||||
|
|
||||||
|
def getPublicUsers(self):
|
||||||
|
|
||||||
|
server = self.getServer()
|
||||||
|
|
||||||
|
# Get public Users
|
||||||
|
url = "%s/emby/Users/Public?format=json" % server
|
||||||
|
result = self.doUtils.downloadUrl(url, authenticate=False)
|
||||||
|
|
||||||
|
if result != "":
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
# Server connection failed
|
||||||
|
return False
|
||||||
|
|
||||||
|
def hasAccess(self):
|
||||||
|
# hasAccess is verified in service.py
|
||||||
|
url = "{server}/emby/Users?format=json"
|
||||||
|
result = self.doUtils.downloadUrl(url)
|
||||||
|
|
||||||
|
if result == False:
|
||||||
|
# Access is restricted, set in downloadutils.py via exception
|
||||||
|
self.logMsg("Access is restricted.", 1)
|
||||||
|
self.HasAccess = False
|
||||||
|
|
||||||
|
elif utils.window('emby_online') != "true":
|
||||||
|
# Server connection failed
|
||||||
|
pass
|
||||||
|
|
||||||
|
elif utils.window('emby_serverStatus') == "restricted":
|
||||||
|
self.logMsg("Access is granted.", 1)
|
||||||
|
self.HasAccess = True
|
||||||
|
utils.window('emby_serverStatus', clear=True)
|
||||||
|
xbmcgui.Dialog().notification("Emby server", "Access is enabled.")
|
||||||
|
|
||||||
|
def loadCurrUser(self, authenticated=False):
|
||||||
|
|
||||||
|
doUtils = self.doUtils
|
||||||
|
username = self.getUsername()
|
||||||
|
userId = self.getUserId()
|
||||||
|
|
||||||
|
# Only to be used if token exists
|
||||||
|
self.currUserId = userId
|
||||||
|
self.currServer = self.getServer()
|
||||||
|
self.currToken = self.getToken()
|
||||||
|
self.ssl = self.getSSLverify()
|
||||||
|
self.sslcert = self.getSSL()
|
||||||
|
|
||||||
|
# Test the validity of current token
|
||||||
|
if authenticated == False:
|
||||||
|
url = "%s/emby/Users/%s?format=json" % (self.currServer, userId)
|
||||||
|
utils.window('emby_currUser', value=userId)
|
||||||
|
utils.window('emby_accessToken%s' % userId, value=self.currToken)
|
||||||
|
result = doUtils.downloadUrl(url)
|
||||||
|
|
||||||
|
if result == 401:
|
||||||
|
# Token is no longer valid
|
||||||
|
self.resetClient()
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Set to windows property
|
||||||
|
utils.window('emby_currUser', value=userId)
|
||||||
|
utils.window('emby_accessToken%s' % userId, value=self.currToken)
|
||||||
|
utils.window('emby_server%s' % userId, value=self.currServer)
|
||||||
|
utils.window('emby_server_%s' % userId, value=self.getServer(prefix=False))
|
||||||
|
|
||||||
|
# Set DownloadUtils values
|
||||||
|
doUtils.setUsername(username)
|
||||||
|
doUtils.setUserId(self.currUserId)
|
||||||
|
doUtils.setServer(self.currServer)
|
||||||
|
doUtils.setToken(self.currToken)
|
||||||
|
doUtils.setSSL(self.ssl, self.sslcert)
|
||||||
|
# parental control - let's verify if access is restricted
|
||||||
|
self.hasAccess()
|
||||||
|
# Start DownloadUtils session
|
||||||
|
doUtils.startSession()
|
||||||
|
self.getAdditionalUsers()
|
||||||
|
# Set user preferences in settings
|
||||||
|
self.currUser = username
|
||||||
|
self.setUserPref()
|
||||||
|
|
||||||
|
|
||||||
|
def authenticate(self):
|
||||||
|
# Get /profile/addon_data
|
||||||
|
addondir = xbmc.translatePath(self.addon.getAddonInfo('profile')).decode('utf-8')
|
||||||
|
hasSettings = xbmcvfs.exists("%ssettings.xml" % addondir)
|
||||||
|
|
||||||
|
username = self.getUsername()
|
||||||
|
server = self.getServer()
|
||||||
|
|
||||||
|
# If there's no settings.xml
|
||||||
|
if not hasSettings:
|
||||||
|
self.logMsg("No settings.xml found.", 1)
|
||||||
|
self.auth = False
|
||||||
|
return
|
||||||
|
# If no user information
|
||||||
|
elif not server or not username:
|
||||||
|
self.logMsg("Missing server information.", 1)
|
||||||
|
self.auth = False
|
||||||
|
return
|
||||||
|
# If there's a token, load the user
|
||||||
|
elif self.getToken():
|
||||||
|
result = self.loadCurrUser()
|
||||||
|
|
||||||
|
if result == False:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
self.logMsg("Current user: %s" % self.currUser, 1)
|
||||||
|
self.logMsg("Current userId: %s" % self.currUserId, 1)
|
||||||
|
self.logMsg("Current accessToken: %s" % self.currToken, 2)
|
||||||
|
return
|
||||||
|
|
||||||
|
##### AUTHENTICATE USER #####
|
||||||
|
|
||||||
|
users = self.getPublicUsers()
|
||||||
|
password = ""
|
||||||
|
|
||||||
|
# Find user in list
|
||||||
|
for user in users:
|
||||||
|
name = user['Name']
|
||||||
|
|
||||||
|
if username.decode('utf-8') in name:
|
||||||
|
# If user has password
|
||||||
|
if user['HasPassword'] == True:
|
||||||
|
password = xbmcgui.Dialog().input(
|
||||||
|
heading="Enter password for user: %s" % username,
|
||||||
|
option=xbmcgui.ALPHANUM_HIDE_INPUT)
|
||||||
|
# If password dialog is cancelled
|
||||||
|
if not password:
|
||||||
|
self.logMsg("No password entered.", 0)
|
||||||
|
utils.window('emby_serverStatus', value="Stop")
|
||||||
|
self.auth = False
|
||||||
|
return
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# Manual login, user is hidden
|
||||||
|
password = xbmcgui.Dialog().input(
|
||||||
|
heading="Enter password for user: %s" % username,
|
||||||
|
option=xbmcgui.ALPHANUM_HIDE_INPUT)
|
||||||
|
sha1 = hashlib.sha1(password)
|
||||||
|
sha1 = sha1.hexdigest()
|
||||||
|
|
||||||
|
# Authenticate username and password
|
||||||
|
url = "%s/emby/Users/AuthenticateByName?format=json" % server
|
||||||
|
data = {'username': username, 'password': sha1}
|
||||||
|
self.logMsg(data, 2)
|
||||||
|
|
||||||
|
result = self.doUtils.downloadUrl(url, postBody=data, type="POST", authenticate=False)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.logMsg("Auth response: %s" % result, 1)
|
||||||
|
accessToken = result['AccessToken']
|
||||||
|
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
self.logMsg("Failed to retrieve the api key.", 1)
|
||||||
|
accessToken = None
|
||||||
|
|
||||||
|
if accessToken is not None:
|
||||||
|
self.currUser = username
|
||||||
|
xbmcgui.Dialog().notification("Emby server", "Welcome %s!" % self.currUser)
|
||||||
|
userId = result['User']['Id']
|
||||||
|
utils.settings('accessToken', value=accessToken)
|
||||||
|
utils.settings('userId%s' % username, value=userId)
|
||||||
|
self.logMsg("User Authenticated: %s" % accessToken, 1)
|
||||||
|
self.loadCurrUser(authenticated=True)
|
||||||
|
utils.window('emby_serverStatus', clear=True)
|
||||||
|
self.retry = 0
|
||||||
|
else:
|
||||||
|
self.logMsg("User authentication failed.", 1)
|
||||||
|
utils.settings('accessToken', value="")
|
||||||
|
utils.settings('userId%s' % username, value="")
|
||||||
|
xbmcgui.Dialog().ok("Error connecting", "Invalid username or password.")
|
||||||
|
|
||||||
|
# Give two attempts at entering password
|
||||||
|
if self.retry == 2:
|
||||||
|
self.logMsg(
|
||||||
|
"""Too many retries. You can retry by resetting
|
||||||
|
attempts in the addon settings.""", 1)
|
||||||
|
utils.window('emby_serverStatus', value="Stop")
|
||||||
|
xbmcgui.Dialog().ok(
|
||||||
|
heading="Error connecting",
|
||||||
|
line1="Failed to authenticate too many times.",
|
||||||
|
line2="You can retry by resetting attempts in the addon settings.")
|
||||||
|
|
||||||
|
self.retry += 1
|
||||||
|
self.auth = False
|
||||||
|
|
||||||
|
def resetClient(self):
|
||||||
|
|
||||||
|
self.logMsg("Reset UserClient authentication.", 1)
|
||||||
|
username = self.getUsername()
|
||||||
|
|
||||||
|
if self.currToken is not None:
|
||||||
|
# In case of 401, removed saved token
|
||||||
|
utils.settings('accessToken', value="")
|
||||||
|
utils.window('emby_accessToken%s' % username, clear=True)
|
||||||
|
self.currToken = None
|
||||||
|
self.logMsg("User token has been removed.", 1)
|
||||||
|
|
||||||
|
self.auth = True
|
||||||
|
self.currUser = None
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
|
||||||
|
monitor = xbmc.Monitor()
|
||||||
|
self.logMsg("----===## Starting UserClient ##===----", 0)
|
||||||
|
|
||||||
|
while not monitor.abortRequested():
|
||||||
|
|
||||||
|
# Verify the log level
|
||||||
|
currLogLevel = self.getLogLevel()
|
||||||
|
if self.logLevel != currLogLevel:
|
||||||
|
# Set new log level
|
||||||
|
self.logLevel = currLogLevel
|
||||||
|
utils.window('emby_logLevel', value=str(currLogLevel))
|
||||||
|
self.logMsg("New Log Level: %s" % currLogLevel, 0)
|
||||||
|
|
||||||
|
|
||||||
|
status = utils.window('emby_serverStatus')
|
||||||
|
if status:
|
||||||
|
# Verify the connection status to server
|
||||||
|
if status == "restricted":
|
||||||
|
# Parental control is restricting access
|
||||||
|
self.HasAccess = False
|
||||||
|
|
||||||
|
elif status == "401":
|
||||||
|
# Unauthorized access, revoke token
|
||||||
|
utils.window('emby_serverStatus', value="Auth")
|
||||||
|
self.resetClient()
|
||||||
|
|
||||||
|
if self.auth and (self.currUser is None):
|
||||||
|
# Try to authenticate user
|
||||||
|
status = utils.window('emby_serverStatus')
|
||||||
|
if not status or status == "Auth":
|
||||||
|
# Set auth flag because we no longer need
|
||||||
|
# to authenticate the user
|
||||||
|
self.auth = False
|
||||||
|
self.authenticate()
|
||||||
|
|
||||||
|
|
||||||
|
if not self.auth and (self.currUser is None):
|
||||||
|
# If authenticate failed.
|
||||||
|
server = self.getServer()
|
||||||
|
username = self.getUsername()
|
||||||
|
status = utils.window('emby_serverStatus')
|
||||||
|
|
||||||
|
# The status Stop is for when user cancelled password dialog.
|
||||||
|
if server and username and status != "Stop":
|
||||||
|
# Only if there's information found to login
|
||||||
|
self.logMsg("Server found: %s" % server, 2)
|
||||||
|
self.logMsg("Username found: %s" % username, 2)
|
||||||
|
self.auth = True
|
||||||
|
|
||||||
|
|
||||||
|
if self.stopClient == True:
|
||||||
|
# If stopping the client didn't work
|
||||||
|
break
|
||||||
|
|
||||||
|
if monitor.waitForAbort(1):
|
||||||
|
# Abort was requested while waiting. We should exit
|
||||||
|
break
|
||||||
|
|
||||||
|
self.doUtils.stopSession()
|
||||||
|
self.logMsg("##===---- UserClient Stopped ----===##", 0)
|
||||||
|
|
||||||
|
def stopClient(self):
|
||||||
|
# When emby for kodi terminates
|
||||||
|
self.stopClient = True
|
487
resources/lib/utils.py
Normal file
487
resources/lib/utils.py
Normal file
|
@ -0,0 +1,487 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
#################################################################################################
|
||||||
|
|
||||||
|
import cProfile
|
||||||
|
import inspect
|
||||||
|
import pstats
|
||||||
|
import sqlite3
|
||||||
|
import time
|
||||||
|
import unicodedata
|
||||||
|
import xml.etree.ElementTree as etree
|
||||||
|
|
||||||
|
import xbmc
|
||||||
|
import xbmcaddon
|
||||||
|
import xbmcgui
|
||||||
|
import xbmcvfs
|
||||||
|
|
||||||
|
#################################################################################################
|
||||||
|
|
||||||
|
|
||||||
|
def logMsg(title, msg, level=1):
|
||||||
|
|
||||||
|
# Get the logLevel set in UserClient
|
||||||
|
try:
|
||||||
|
logLevel = int(window('emby_logLevel'))
|
||||||
|
except ValueError:
|
||||||
|
logLevel = 0
|
||||||
|
|
||||||
|
if logLevel >= level:
|
||||||
|
|
||||||
|
if logLevel == 2: # inspect.stack() is expensive
|
||||||
|
try:
|
||||||
|
xbmc.log("%s -> %s : %s" % (title, inspect.stack()[1][3], msg))
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
xbmc.log("%s -> %s : %s" % (title, inspect.stack()[1][3], msg.encode('utf-8')))
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
xbmc.log("%s -> %s" % (title, msg))
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
xbmc.log("%s -> %s" % (title, msg.encode('utf-8')))
|
||||||
|
|
||||||
|
def window(property, value=None, clear=False, windowid=10000):
|
||||||
|
# Get or set window property
|
||||||
|
WINDOW = xbmcgui.Window(windowid)
|
||||||
|
|
||||||
|
if clear:
|
||||||
|
WINDOW.clearProperty(property)
|
||||||
|
elif value is not None:
|
||||||
|
WINDOW.setProperty(property, value)
|
||||||
|
else:
|
||||||
|
return WINDOW.getProperty(property)
|
||||||
|
|
||||||
|
def settings(setting, value=None):
|
||||||
|
# Get or add addon setting
|
||||||
|
addon = xbmcaddon.Addon(id='plugin.video.emby')
|
||||||
|
|
||||||
|
if value is not None:
|
||||||
|
addon.setSetting(setting, value)
|
||||||
|
else:
|
||||||
|
return addon.getSetting(setting)
|
||||||
|
|
||||||
|
def language(stringid):
|
||||||
|
# Central string retrieval
|
||||||
|
addon = xbmcaddon.Addon(id='plugin.video.emby')
|
||||||
|
string = addon.getLocalizedString(stringid)
|
||||||
|
|
||||||
|
return string
|
||||||
|
|
||||||
|
def kodiSQL(type="video"):
|
||||||
|
|
||||||
|
if type == "emby":
|
||||||
|
dbPath = xbmc.translatePath("special://database/emby.db").decode('utf-8')
|
||||||
|
elif type == "music":
|
||||||
|
dbPath = getKodiMusicDBPath()
|
||||||
|
elif type == "texture":
|
||||||
|
dbPath = xbmc.translatePath("special://database/Textures13.db").decode('utf-8')
|
||||||
|
else:
|
||||||
|
dbPath = getKodiVideoDBPath()
|
||||||
|
|
||||||
|
connection = sqlite3.connect(dbPath)
|
||||||
|
return connection
|
||||||
|
|
||||||
|
def getKodiVideoDBPath():
|
||||||
|
|
||||||
|
kodibuild = xbmc.getInfoLabel('System.BuildVersion')[:2]
|
||||||
|
dbVersion = {
|
||||||
|
|
||||||
|
"13": 78, # Gotham
|
||||||
|
"14": 90, # Helix
|
||||||
|
"15": 93, # Isengard
|
||||||
|
"16": 99 # Jarvis
|
||||||
|
}
|
||||||
|
|
||||||
|
dbPath = xbmc.translatePath(
|
||||||
|
"special://database/MyVideos%s.db"
|
||||||
|
% dbVersion.get(kodibuild, "")).decode('utf-8')
|
||||||
|
return dbPath
|
||||||
|
|
||||||
|
def getKodiMusicDBPath():
|
||||||
|
|
||||||
|
kodibuild = xbmc.getInfoLabel('System.BuildVersion')[:2]
|
||||||
|
dbVersion = {
|
||||||
|
|
||||||
|
"13": 46, # Gotham
|
||||||
|
"14": 48, # Helix
|
||||||
|
"15": 52, # Isengard
|
||||||
|
"16": 56 # Jarvis
|
||||||
|
}
|
||||||
|
|
||||||
|
dbPath = xbmc.translatePath(
|
||||||
|
"special://database/MyMusic%s.db"
|
||||||
|
% dbVersion.get(kodibuild, "")).decode('utf-8')
|
||||||
|
return dbPath
|
||||||
|
|
||||||
|
def reset():
|
||||||
|
|
||||||
|
dialog = xbmcgui.Dialog()
|
||||||
|
|
||||||
|
resp = dialog.yesno("Warning", "Are you sure you want to reset your local Kodi database?")
|
||||||
|
if resp == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
# first stop any db sync
|
||||||
|
window('emby_shouldStop', value="true")
|
||||||
|
count = 10
|
||||||
|
while window('emby_dbScan') == "true":
|
||||||
|
logMsg("EMBY", "Sync is running, will retry: %s..." % count)
|
||||||
|
count -= 1
|
||||||
|
if count == 0:
|
||||||
|
dialog.ok("Warning", "Could not stop the database from running. Try again.")
|
||||||
|
return
|
||||||
|
xbmc.sleep(1000)
|
||||||
|
|
||||||
|
# Clean up the playlists
|
||||||
|
path = xbmc.translatePath("special://profile/playlists/video/").decode('utf-8')
|
||||||
|
dirs, files = xbmcvfs.listdir(path)
|
||||||
|
for file in files:
|
||||||
|
if file.startswith('Emby'):
|
||||||
|
xbmcvfs.delete("%s%s" % (path, file))
|
||||||
|
|
||||||
|
# Clean up the video nodes
|
||||||
|
import shutil
|
||||||
|
path = xbmc.translatePath("special://profile/library/video/").decode('utf-8')
|
||||||
|
dirs, files = xbmcvfs.listdir(path)
|
||||||
|
for dir in dirs:
|
||||||
|
if dir.startswith('Emby'):
|
||||||
|
shutil.rmtree("%s%s" % (path, dir))
|
||||||
|
for file in files:
|
||||||
|
if file.startswith('emby'):
|
||||||
|
xbmcvfs.delete("%s%s" % (path, file))
|
||||||
|
|
||||||
|
# Wipe the kodi databases
|
||||||
|
logMsg("EMBY", "Resetting the Kodi video database.")
|
||||||
|
connection = kodiSQL('video')
|
||||||
|
cursor = connection.cursor()
|
||||||
|
cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"')
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
for row in rows:
|
||||||
|
tablename = row[0]
|
||||||
|
if tablename != "version":
|
||||||
|
cursor.execute("DELETE FROM " + tablename)
|
||||||
|
connection.commit()
|
||||||
|
cursor.close()
|
||||||
|
|
||||||
|
if settings('disableMusic') != "true":
|
||||||
|
logMsg("EMBY", "Resetting the Kodi music database.")
|
||||||
|
connection = kodiSQL('music')
|
||||||
|
cursor = connection.cursor()
|
||||||
|
cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"')
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
for row in rows:
|
||||||
|
tablename = row[0]
|
||||||
|
if tablename != "version":
|
||||||
|
cursor.execute("DELETE FROM " + tablename)
|
||||||
|
connection.commit()
|
||||||
|
cursor.close()
|
||||||
|
|
||||||
|
# Wipe the emby database
|
||||||
|
logMsg("EMBY", "Resetting the Emby database.")
|
||||||
|
connection = kodiSQL('emby')
|
||||||
|
cursor = connection.cursor()
|
||||||
|
cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"')
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
for row in rows:
|
||||||
|
tablename = row[0]
|
||||||
|
if tablename != "version":
|
||||||
|
cursor.execute("DELETE FROM " + tablename)
|
||||||
|
connection.commit()
|
||||||
|
cursor.close()
|
||||||
|
|
||||||
|
# reset the install run flag
|
||||||
|
settings('SyncInstallRunDone', value="false")
|
||||||
|
|
||||||
|
# Remove emby info
|
||||||
|
resp = dialog.yesno("Warning", "Reset all Emby Addon settings?")
|
||||||
|
if resp == 1:
|
||||||
|
# Delete the settings
|
||||||
|
addon = xbmcaddon.Addon()
|
||||||
|
addondir = xbmc.translatePath(addon.getAddonInfo('profile')).decode('utf-8')
|
||||||
|
dataPath = "%ssettings.xml" % addondir
|
||||||
|
xbmcvfs.delete(dataPath)
|
||||||
|
logMsg("EMBY", "Deleting: settings.xml", 1)
|
||||||
|
|
||||||
|
dialog.ok(
|
||||||
|
heading="Emby for Kodi",
|
||||||
|
line1="Database reset has completed, Kodi will now restart to apply the changes.")
|
||||||
|
xbmc.executebuiltin('RestartApp')
|
||||||
|
|
||||||
|
def startProfiling():
|
||||||
|
|
||||||
|
pr = cProfile.Profile()
|
||||||
|
pr.enable()
|
||||||
|
|
||||||
|
return pr
|
||||||
|
|
||||||
|
def stopProfiling(pr, profileName):
|
||||||
|
|
||||||
|
pr.disable()
|
||||||
|
ps = pstats.Stats(pr)
|
||||||
|
|
||||||
|
profiles = xbmc.translatePath("%sprofiles/"
|
||||||
|
% xbmcaddon.Addon().getAddonInfo('profile')).decode('utf-8')
|
||||||
|
|
||||||
|
if not xbmcvfs.exists(profiles):
|
||||||
|
# Create the profiles folder
|
||||||
|
xbmcvfs.mkdir(profiles)
|
||||||
|
|
||||||
|
timestamp = time.strftime("%Y-%m-%d %H-%M-%S")
|
||||||
|
profile = "%s%s_profile_(%s).tab" % (profiles, profileName, timestamp)
|
||||||
|
|
||||||
|
f = open(profile, 'wb')
|
||||||
|
f.write("NumbCalls\tTotalTime\tCumulativeTime\tFunctionName\tFileName\r\n")
|
||||||
|
for (key, value) in ps.stats.items():
|
||||||
|
(filename, count, func_name) = key
|
||||||
|
(ccalls, ncalls, total_time, cumulative_time, callers) = value
|
||||||
|
try:
|
||||||
|
f.write(
|
||||||
|
"%s\t%s\t%s\t%s\t%s\r\n"
|
||||||
|
% (ncalls, "{:10.4f}".format(total_time),
|
||||||
|
"{:10.4f}".format(cumulative_time), func_name, filename))
|
||||||
|
except ValueError:
|
||||||
|
f.write(
|
||||||
|
"%s\t%s\t%s\t%s\t%s\r\n"
|
||||||
|
% (ncalls, "{0}".format(total_time),
|
||||||
|
"{0}".format(cumulative_time), func_name, filename))
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
def normalize_nodes(text):
|
||||||
|
# For video nodes
|
||||||
|
text = text.replace(":", "")
|
||||||
|
text = text.replace("/", "-")
|
||||||
|
text = text.replace("\\", "-")
|
||||||
|
text = text.replace("<", "")
|
||||||
|
text = text.replace(">", "")
|
||||||
|
text = text.replace("*", "")
|
||||||
|
text = text.replace("?", "")
|
||||||
|
text = text.replace('|', "")
|
||||||
|
text = text.replace('(', "")
|
||||||
|
text = text.replace(')', "")
|
||||||
|
text = text.strip()
|
||||||
|
# Remove dots from the last character as windows can not have directories
|
||||||
|
# with dots at the end
|
||||||
|
text = text.rstrip('.')
|
||||||
|
text = unicodedata.normalize('NFKD', unicode(text, 'utf-8')).encode('ascii', 'ignore')
|
||||||
|
|
||||||
|
return text
|
||||||
|
|
||||||
|
def normalize_string(text):
|
||||||
|
# For theme media, do not modify unless
|
||||||
|
# modified in TV Tunes
|
||||||
|
text = text.replace(":", "")
|
||||||
|
text = text.replace("/", "-")
|
||||||
|
text = text.replace("\\", "-")
|
||||||
|
text = text.replace("<", "")
|
||||||
|
text = text.replace(">", "")
|
||||||
|
text = text.replace("*", "")
|
||||||
|
text = text.replace("?", "")
|
||||||
|
text = text.replace('|', "")
|
||||||
|
text = text.strip()
|
||||||
|
# Remove dots from the last character as windows can not have directories
|
||||||
|
# with dots at the end
|
||||||
|
text = text.rstrip('.')
|
||||||
|
text = unicodedata.normalize('NFKD', unicode(text, 'utf-8')).encode('ascii', 'ignore')
|
||||||
|
|
||||||
|
return text
|
||||||
|
|
||||||
|
def indent(elem, level=0):
|
||||||
|
# Prettify xml trees
|
||||||
|
i = "\n" + level*" "
|
||||||
|
if len(elem):
|
||||||
|
if not elem.text or not elem.text.strip():
|
||||||
|
elem.text = i + " "
|
||||||
|
if not elem.tail or not elem.tail.strip():
|
||||||
|
elem.tail = i
|
||||||
|
for elem in elem:
|
||||||
|
indent(elem, level+1)
|
||||||
|
if not elem.tail or not elem.tail.strip():
|
||||||
|
elem.tail = i
|
||||||
|
else:
|
||||||
|
if level and (not elem.tail or not elem.tail.strip()):
|
||||||
|
elem.tail = i
|
||||||
|
|
||||||
|
def sourcesXML():
|
||||||
|
# To make Master lock compatible
|
||||||
|
path = xbmc.translatePath("special://profile/").decode('utf-8')
|
||||||
|
xmlpath = "%ssources.xml" % path
|
||||||
|
|
||||||
|
try:
|
||||||
|
xmlparse = etree.parse(xmlpath)
|
||||||
|
except: # Document is blank or missing
|
||||||
|
root = etree.Element('sources')
|
||||||
|
else:
|
||||||
|
root = xmlparse.getroot()
|
||||||
|
|
||||||
|
|
||||||
|
video = root.find('video')
|
||||||
|
if video is None:
|
||||||
|
video = etree.SubElement(root, 'video')
|
||||||
|
etree.SubElement(video, 'default', attrib={'pathversion': "1"})
|
||||||
|
|
||||||
|
# Add elements
|
||||||
|
for i in range(1, 3):
|
||||||
|
|
||||||
|
for source in root.findall('.//path'):
|
||||||
|
if source.text == "smb://embydummy/dummypath%s/" % i:
|
||||||
|
# Already there, skip
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
source = etree.SubElement(video, 'source')
|
||||||
|
etree.SubElement(source, 'name').text = "Emby"
|
||||||
|
etree.SubElement(source, 'path', attrib={'pathversion': "1"}).text = (
|
||||||
|
|
||||||
|
"smb://embydummy/dummypath%s/" % i
|
||||||
|
)
|
||||||
|
etree.SubElement(source, 'allowsharing').text = "true"
|
||||||
|
# Prettify and write to file
|
||||||
|
try:
|
||||||
|
indent(root)
|
||||||
|
except: pass
|
||||||
|
etree.ElementTree(root).write(xmlpath)
|
||||||
|
|
||||||
|
def passwordsXML():
|
||||||
|
|
||||||
|
# To add network credentials
|
||||||
|
path = xbmc.translatePath("special://userdata/").decode('utf-8')
|
||||||
|
xmlpath = "%spasswords.xml" % path
|
||||||
|
|
||||||
|
try:
|
||||||
|
xmlparse = etree.parse(xmlpath)
|
||||||
|
except: # Document is blank or missing
|
||||||
|
root = etree.Element('passwords')
|
||||||
|
else:
|
||||||
|
root = xmlparse.getroot()
|
||||||
|
|
||||||
|
dialog = xbmcgui.Dialog()
|
||||||
|
credentials = settings('networkCreds')
|
||||||
|
if credentials:
|
||||||
|
# Present user with options
|
||||||
|
option = dialog.select("Modify/Remove network credentials", ["Modify", "Remove"])
|
||||||
|
|
||||||
|
if option < 0:
|
||||||
|
# User cancelled dialog
|
||||||
|
return
|
||||||
|
|
||||||
|
elif option == 1:
|
||||||
|
# User selected remove
|
||||||
|
iterator = root.getiterator('passwords')
|
||||||
|
|
||||||
|
for paths in iterator:
|
||||||
|
for path in paths:
|
||||||
|
if path.find('.//from').text == "smb://%s/" % credentials:
|
||||||
|
paths.remove(path)
|
||||||
|
logMsg("EMBY", "Successfully removed credentials for: %s"
|
||||||
|
% credentials, 1)
|
||||||
|
etree.ElementTree(root).write(xmlpath)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
logMsg("EMBY", "Failed to find saved server: %s in passwords.xml" % credentials, 1)
|
||||||
|
|
||||||
|
settings('networkCreds', value="")
|
||||||
|
xbmcgui.Dialog().notification(
|
||||||
|
heading="Emby for Kodi",
|
||||||
|
message="%s removed from passwords.xml!" % credentials,
|
||||||
|
icon="special://home/addons/plugin.video.emby/icon.png",
|
||||||
|
time=1000,
|
||||||
|
sound=False)
|
||||||
|
return
|
||||||
|
|
||||||
|
elif option == 0:
|
||||||
|
# User selected to modify
|
||||||
|
server = dialog.input("Modify the computer name or ip address", credentials)
|
||||||
|
if not server:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
# No credentials added
|
||||||
|
dialog.ok(
|
||||||
|
heading="Network credentials",
|
||||||
|
line1= (
|
||||||
|
"Input the server name or IP address as indicated in your emby library paths. "
|
||||||
|
'For example, the server name: \\\\SERVER-PC\\path\\ is "SERVER-PC".'))
|
||||||
|
server = dialog.input("Enter the server name or IP address", settings('ipaddress'))
|
||||||
|
if not server:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Network username
|
||||||
|
user = dialog.input("Enter the network username")
|
||||||
|
if not user:
|
||||||
|
return
|
||||||
|
# Network password
|
||||||
|
password = dialog.input(
|
||||||
|
heading="Enter the network password",
|
||||||
|
option=xbmcgui.ALPHANUM_HIDE_INPUT)
|
||||||
|
if not password:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Add elements
|
||||||
|
for path in root.findall('.//path'):
|
||||||
|
if path.find('.//from').text.lower() == "smb://%s/" % server.lower():
|
||||||
|
# Found the server, rewrite credentials
|
||||||
|
path.find('.//to').text = "smb://%s:%s@%s/" % (user, password, server)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# Server not found, add it.
|
||||||
|
path = etree.SubElement(root, 'path')
|
||||||
|
etree.SubElement(path, 'from', attrib={'pathversion': "1"}).text = "smb://%s/" % server
|
||||||
|
topath = "smb://%s:%s@%s/" % (user, password, server)
|
||||||
|
etree.SubElement(path, 'to', attrib={'pathversion': "1"}).text = topath
|
||||||
|
# Force Kodi to see the credentials without restarting
|
||||||
|
xbmcvfs.exists(topath)
|
||||||
|
|
||||||
|
# Add credentials
|
||||||
|
settings('networkCreds', value="%s" % server)
|
||||||
|
logMsg("EMBY", "Added server: %s to passwords.xml" % server, 1)
|
||||||
|
# Prettify and write to file
|
||||||
|
try:
|
||||||
|
indent(root)
|
||||||
|
except: pass
|
||||||
|
etree.ElementTree(root).write(xmlpath)
|
||||||
|
|
||||||
|
dialog.notification(
|
||||||
|
heading="Emby for Kodi",
|
||||||
|
message="%s added to passwords.xml!" % server,
|
||||||
|
icon="special://home/addons/plugin.video.emby/icon.png",
|
||||||
|
time=1000,
|
||||||
|
sound=False)
|
||||||
|
|
||||||
|
def playlistXSP(mediatype, tagname, viewtype="", delete=False):
|
||||||
|
# Tagname is in unicode - actions: add or delete
|
||||||
|
tagname = tagname.encode('utf-8')
|
||||||
|
cleantagname = normalize_nodes(tagname)
|
||||||
|
path = xbmc.translatePath("special://profile/playlists/video/").decode('utf-8')
|
||||||
|
if viewtype == "mixed":
|
||||||
|
plname = "%s - %s" % (tagname, mediatype)
|
||||||
|
xsppath = "%sEmby %s - %s.xsp" % (path, cleantagname, mediatype)
|
||||||
|
else:
|
||||||
|
plname = tagname
|
||||||
|
xsppath = "%sEmby %s.xsp" % (path, cleantagname)
|
||||||
|
|
||||||
|
# Create the playlist directory
|
||||||
|
if not xbmcvfs.exists(path):
|
||||||
|
xbmcvfs.mkdirs(path)
|
||||||
|
|
||||||
|
# Only add the playlist if it doesn't already exists
|
||||||
|
if xbmcvfs.exists(xsppath):
|
||||||
|
|
||||||
|
if delete:
|
||||||
|
xbmcvfs.delete(xsppath)
|
||||||
|
logMsg("EMBY", "Successfully removed playlist: %s." % tagname, 1)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
# Using write process since there's no guarantee the xml declaration works with etree
|
||||||
|
itemtypes = {
|
||||||
|
'homevideos': "movies"
|
||||||
|
}
|
||||||
|
f = open(xsppath, 'w')
|
||||||
|
f.write(
|
||||||
|
'<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>\n'
|
||||||
|
'<smartplaylist type="%s">\n\t'
|
||||||
|
'<name>Emby %s</name>\n\t'
|
||||||
|
'<match>all</match>\n\t'
|
||||||
|
'<rule field="tag" operator="is">\n\t\t'
|
||||||
|
'<value>%s</value>\n\t'
|
||||||
|
'</rule>'
|
||||||
|
% (itemtypes.get(mediatype, mediatype), plname, tagname))
|
||||||
|
f.close()
|
||||||
|
logMsg("EMBY", "Successfully added playlist: %s" % tagname)
|
344
resources/lib/videonodes.py
Normal file
344
resources/lib/videonodes.py
Normal file
|
@ -0,0 +1,344 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
#################################################################################################
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
import xml.etree.ElementTree as etree
|
||||||
|
|
||||||
|
import xbmc
|
||||||
|
import xbmcaddon
|
||||||
|
import xbmcvfs
|
||||||
|
|
||||||
|
import clientinfo
|
||||||
|
import utils
|
||||||
|
|
||||||
|
#################################################################################################
|
||||||
|
|
||||||
|
|
||||||
|
class VideoNodes(object):
|
||||||
|
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
|
||||||
|
clientInfo = clientinfo.ClientInfo()
|
||||||
|
self.addonName = clientInfo.getAddonName()
|
||||||
|
|
||||||
|
self.kodiversion = int(xbmc.getInfoLabel("System.BuildVersion")[:2])
|
||||||
|
|
||||||
|
def logMsg(self, msg, lvl=1):
|
||||||
|
|
||||||
|
className = self.__class__.__name__
|
||||||
|
utils.logMsg("%s %s" % (self.addonName, className), msg, lvl)
|
||||||
|
|
||||||
|
|
||||||
|
def commonRoot(self, order, label, tagname, roottype=1):
|
||||||
|
|
||||||
|
if roottype == 0:
|
||||||
|
# Index
|
||||||
|
root = etree.Element('node', attrib={'order': "%s" % order})
|
||||||
|
elif roottype == 1:
|
||||||
|
# Filter
|
||||||
|
root = etree.Element('node', attrib={'order': "%s" % order, 'type': "filter"})
|
||||||
|
etree.SubElement(root, 'match').text = "all"
|
||||||
|
# Add tag rule
|
||||||
|
rule = etree.SubElement(root, 'rule', attrib={'field': "tag", 'operator': "is"})
|
||||||
|
etree.SubElement(rule, 'value').text = tagname
|
||||||
|
else:
|
||||||
|
# Folder
|
||||||
|
root = etree.Element('node', attrib={'order': "%s" % order, 'type': "folder"})
|
||||||
|
|
||||||
|
etree.SubElement(root, 'label').text = label
|
||||||
|
etree.SubElement(root, 'icon').text = "special://home/addons/plugin.video.emby/icon.png"
|
||||||
|
|
||||||
|
return root
|
||||||
|
|
||||||
|
def viewNode(self, indexnumber, tagname, mediatype, viewtype, delete=False):
|
||||||
|
|
||||||
|
kodiversion = self.kodiversion
|
||||||
|
|
||||||
|
if mediatype == "homevideos":
|
||||||
|
# Treat homevideos as movies
|
||||||
|
mediatype = "movies"
|
||||||
|
|
||||||
|
tagname = tagname.encode('utf-8')
|
||||||
|
cleantagname = utils.normalize_nodes(tagname)
|
||||||
|
if viewtype == "mixed":
|
||||||
|
dirname = "%s - %s" % (cleantagname, mediatype)
|
||||||
|
else:
|
||||||
|
dirname = cleantagname
|
||||||
|
|
||||||
|
path = xbmc.translatePath("special://profile/library/video/").decode('utf-8')
|
||||||
|
nodepath = xbmc.translatePath(
|
||||||
|
"special://profile/library/video/Emby - %s/" % dirname).decode('utf-8')
|
||||||
|
|
||||||
|
# Verify the video directory
|
||||||
|
if not xbmcvfs.exists(path):
|
||||||
|
shutil.copytree(
|
||||||
|
src=xbmc.translatePath("special://xbmc/system/library/video/").decode('utf-8'),
|
||||||
|
dst=xbmc.translatePath("special://profile/library/video/").decode('utf-8'))
|
||||||
|
xbmcvfs.exists(path)
|
||||||
|
|
||||||
|
# Create the node directory
|
||||||
|
if not xbmcvfs.exists(nodepath):
|
||||||
|
# We need to copy over the default items
|
||||||
|
xbmcvfs.mkdirs(nodepath)
|
||||||
|
else:
|
||||||
|
if delete:
|
||||||
|
dirs, files = xbmcvfs.listdir(nodepath)
|
||||||
|
for file in files:
|
||||||
|
xbmcvfs.delete(nodepath + file)
|
||||||
|
|
||||||
|
self.logMsg("Sucessfully removed videonode: %s." % tagname, 1)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Create index entry
|
||||||
|
nodeXML = "%sindex.xml" % nodepath
|
||||||
|
# Set windows property
|
||||||
|
path = "library://video/Emby - %s/" % dirname
|
||||||
|
for i in range(1, indexnumber):
|
||||||
|
# Verify to make sure we don't create duplicates
|
||||||
|
if utils.window('Emby.nodes.%s.index' % i) == path:
|
||||||
|
return
|
||||||
|
|
||||||
|
utils.window('Emby.nodes.%s.index' % indexnumber, value=path)
|
||||||
|
# Root
|
||||||
|
root = self.commonRoot(order=0, label=dirname, tagname=tagname, roottype=0)
|
||||||
|
try:
|
||||||
|
utils.indent(root)
|
||||||
|
except: pass
|
||||||
|
etree.ElementTree(root).write(nodeXML)
|
||||||
|
|
||||||
|
|
||||||
|
nodetypes = {
|
||||||
|
|
||||||
|
'1': "all",
|
||||||
|
'2': "recent",
|
||||||
|
'3': "recentepisodes",
|
||||||
|
'4': "inprogress",
|
||||||
|
'5': "inprogressepisodes",
|
||||||
|
'6': "unwatched",
|
||||||
|
'7': "nextupepisodes",
|
||||||
|
'8': "sets",
|
||||||
|
'9': "genres",
|
||||||
|
'10': "random",
|
||||||
|
'11': "recommended"
|
||||||
|
}
|
||||||
|
mediatypes = {
|
||||||
|
# label according to nodetype per mediatype
|
||||||
|
'movies': {
|
||||||
|
'1': tagname,
|
||||||
|
'2': 30174,
|
||||||
|
'4': 30177,
|
||||||
|
'6': 30189,
|
||||||
|
'8': 20434,
|
||||||
|
'9': 135,
|
||||||
|
'10': 30229,
|
||||||
|
'11': 30230},
|
||||||
|
|
||||||
|
'tvshows': {
|
||||||
|
'1': tagname,
|
||||||
|
'2': 30170,
|
||||||
|
'3': 30175,
|
||||||
|
'4': 30171,
|
||||||
|
'5': 30178,
|
||||||
|
'7': 30179,
|
||||||
|
'9': 135,
|
||||||
|
'10': 30229,
|
||||||
|
'11': 30230},
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes = mediatypes[mediatype]
|
||||||
|
for node in nodes:
|
||||||
|
|
||||||
|
nodetype = nodetypes[node]
|
||||||
|
nodeXML = "%s%s_%s.xml" % (nodepath, cleantagname, nodetype)
|
||||||
|
# Get label
|
||||||
|
stringid = nodes[node]
|
||||||
|
if node != '1':
|
||||||
|
label = utils.language(stringid)
|
||||||
|
if not label:
|
||||||
|
label = xbmc.getLocalizedString(stringid)
|
||||||
|
else:
|
||||||
|
label = stringid
|
||||||
|
|
||||||
|
# Set window properties
|
||||||
|
if nodetype == "nextupepisodes":
|
||||||
|
# Custom query
|
||||||
|
path = "plugin://plugin.video.emby/?id=%s&mode=nextup&limit=25" % tagname
|
||||||
|
elif kodiversion == 14 and nodetype == "recentepisodes":
|
||||||
|
# Custom query
|
||||||
|
path = "plugin://plugin.video.emby/?id=%s&mode=recentepisodes&limit=25" % tagname
|
||||||
|
elif kodiversion == 14 and nodetype == "inprogressepisodes":
|
||||||
|
# Custom query
|
||||||
|
path = "plugin://plugin.video.emby/?id=%s&mode=inprogressepisodes&limit=25"% tagname
|
||||||
|
else:
|
||||||
|
path = "library://video/Emby - %s/%s_%s.xml" % (dirname, cleantagname, nodetype)
|
||||||
|
windowpath = "ActivateWindow(Video, %s, return)" % path
|
||||||
|
|
||||||
|
if nodetype == "all":
|
||||||
|
|
||||||
|
if viewtype == "mixed":
|
||||||
|
templabel = dirname
|
||||||
|
else:
|
||||||
|
templabel = label
|
||||||
|
|
||||||
|
embynode = "Emby.nodes.%s" % indexnumber
|
||||||
|
utils.window('%s.title' % embynode, value=templabel)
|
||||||
|
utils.window('%s.path' % embynode, value=windowpath)
|
||||||
|
utils.window('%s.content' % embynode, value=path)
|
||||||
|
utils.window('%s.type' % embynode, value=mediatype)
|
||||||
|
else:
|
||||||
|
embynode = "Emby.nodes.%s.%s" % (indexnumber, nodetype)
|
||||||
|
utils.window('%s.title' % embynode, value=label)
|
||||||
|
utils.window('%s.path' % embynode, value=windowpath)
|
||||||
|
utils.window('%s.content' % embynode, value=path)
|
||||||
|
|
||||||
|
if xbmcvfs.exists(nodeXML):
|
||||||
|
# Don't recreate xml if already exists
|
||||||
|
continue
|
||||||
|
|
||||||
|
|
||||||
|
# Create the root
|
||||||
|
if nodetype == "nextupepisodes" or (kodiversion == 14 and
|
||||||
|
nodetype in ('recentepisodes', 'inprogressepisodes')):
|
||||||
|
# Folder type with plugin path
|
||||||
|
root = self.commonRoot(order=node, label=label, tagname=tagname, roottype=2)
|
||||||
|
etree.SubElement(root, 'path').text = path
|
||||||
|
etree.SubElement(root, 'content').text = "episodes"
|
||||||
|
else:
|
||||||
|
root = self.commonRoot(order=node, label=label, tagname=tagname)
|
||||||
|
if nodetype in ('recentepisodes', 'inprogressepisodes'):
|
||||||
|
etree.SubElement(root, 'content').text = "episodes"
|
||||||
|
else:
|
||||||
|
etree.SubElement(root, 'content').text = mediatype
|
||||||
|
|
||||||
|
limit = "25"
|
||||||
|
# Elements per nodetype
|
||||||
|
if nodetype == "all":
|
||||||
|
etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle"
|
||||||
|
|
||||||
|
elif nodetype == "recent":
|
||||||
|
etree.SubElement(root, 'order', {'direction': "descending"}).text = "dateadded"
|
||||||
|
etree.SubElement(root, 'limit').text = limit
|
||||||
|
rule = etree.SubElement(root, 'rule', {'field': "playcount", 'operator': "is"})
|
||||||
|
etree.SubElement(rule, 'value').text = "0"
|
||||||
|
|
||||||
|
elif nodetype == "inprogress":
|
||||||
|
etree.SubElement(root, 'rule', {'field': "inprogress", 'operator': "true"})
|
||||||
|
etree.SubElement(root, 'limit').text = limit
|
||||||
|
|
||||||
|
elif nodetype == "genres":
|
||||||
|
etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle"
|
||||||
|
etree.SubElement(root, 'group').text = "genres"
|
||||||
|
|
||||||
|
elif nodetype == "unwatched":
|
||||||
|
etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle"
|
||||||
|
rule = etree.SubElement(root, "rule", {'field': "playcount", 'operator': "is"})
|
||||||
|
etree.SubElement(rule, 'value').text = "0"
|
||||||
|
|
||||||
|
elif nodetype == "sets":
|
||||||
|
etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle"
|
||||||
|
etree.SubElement(root, 'group').text = "sets"
|
||||||
|
|
||||||
|
elif nodetype == "random":
|
||||||
|
etree.SubElement(root, 'order', {'direction': "ascending"}).text = "random"
|
||||||
|
etree.SubElement(root, 'limit').text = limit
|
||||||
|
|
||||||
|
elif nodetype == "recommended":
|
||||||
|
etree.SubElement(root, 'order', {'direction': "descending"}).text = "rating"
|
||||||
|
etree.SubElement(root, 'limit').text = limit
|
||||||
|
rule = etree.SubElement(root, 'rule', {'field': "playcount", 'operator': "is"})
|
||||||
|
etree.SubElement(rule, 'value').text = "0"
|
||||||
|
rule2 = etree.SubElement(root, 'rule',
|
||||||
|
attrib={'field': "rating", 'operator': "greaterthan"})
|
||||||
|
etree.SubElement(rule2, 'value').text = "7"
|
||||||
|
|
||||||
|
elif nodetype == "recentepisodes":
|
||||||
|
# Kodi Isengard, Jarvis
|
||||||
|
etree.SubElement(root, 'order', {'direction': "descending"}).text = "dateadded"
|
||||||
|
etree.SubElement(root, 'limit').text = limit
|
||||||
|
rule = etree.SubElement(root, 'rule', {'field': "playcount", 'operator': "is"})
|
||||||
|
etree.SubElement(rule, 'value').text = "0"
|
||||||
|
|
||||||
|
elif nodetype == "inprogressepisodes":
|
||||||
|
# Kodi Isengard, Jarvis
|
||||||
|
etree.SubElement(root, 'limit').text = "25"
|
||||||
|
rule = etree.SubElement(root, 'rule',
|
||||||
|
attrib={'field': "inprogress", 'operator':"true"})
|
||||||
|
|
||||||
|
try:
|
||||||
|
utils.indent(root)
|
||||||
|
except: pass
|
||||||
|
etree.ElementTree(root).write(nodeXML)
|
||||||
|
|
||||||
|
def singleNode(self, indexnumber, tagname, mediatype, itemtype):
|
||||||
|
|
||||||
|
tagname = tagname.encode('utf-8')
|
||||||
|
cleantagname = utils.normalize_nodes(tagname)
|
||||||
|
nodepath = xbmc.translatePath("special://profile/library/video/").decode('utf-8')
|
||||||
|
nodeXML = "%semby_%s.xml" % (nodepath, cleantagname)
|
||||||
|
path = "library://video/emby_%s.xml" % (cleantagname)
|
||||||
|
windowpath = "ActivateWindow(Video, %s, return)" % path
|
||||||
|
|
||||||
|
# Create the video node directory
|
||||||
|
if not xbmcvfs.exists(nodepath):
|
||||||
|
# We need to copy over the default items
|
||||||
|
shutil.copytree(
|
||||||
|
src=xbmc.translatePath("special://xbmc/system/library/video").decode('utf-8'),
|
||||||
|
dst=xbmc.translatePath("special://profile/library/video").decode('utf-8'))
|
||||||
|
xbmcvfs.exists(path)
|
||||||
|
|
||||||
|
labels = {
|
||||||
|
|
||||||
|
'Favorite movies': 30180,
|
||||||
|
'Favorite tvshows': 30181,
|
||||||
|
'channels': 30173
|
||||||
|
}
|
||||||
|
label = utils.language(labels[tagname])
|
||||||
|
embynode = "Emby.nodes.%s" % indexnumber
|
||||||
|
utils.window('%s.title' % embynode, value=label)
|
||||||
|
utils.window('%s.path' % embynode, value=windowpath)
|
||||||
|
utils.window('%s.content' % embynode, value=path)
|
||||||
|
utils.window('%s.type' % embynode, value=itemtype)
|
||||||
|
|
||||||
|
if xbmcvfs.exists(nodeXML):
|
||||||
|
# Don't recreate xml if already exists
|
||||||
|
return
|
||||||
|
|
||||||
|
if itemtype == "channels":
|
||||||
|
root = self.commonRoot(order=1, label=label, tagname=tagname, roottype=2)
|
||||||
|
etree.SubElement(root, 'path').text = "plugin://plugin.video.emby/?id=0&mode=channels"
|
||||||
|
else:
|
||||||
|
root = self.commonRoot(order=1, label=label, tagname=tagname)
|
||||||
|
etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle"
|
||||||
|
|
||||||
|
etree.SubElement(root, 'content').text = mediatype
|
||||||
|
|
||||||
|
try:
|
||||||
|
utils.indent(root)
|
||||||
|
except: pass
|
||||||
|
etree.ElementTree(root).write(nodeXML)
|
||||||
|
|
||||||
|
def clearProperties(self):
|
||||||
|
|
||||||
|
self.logMsg("Clearing nodes properties.", 1)
|
||||||
|
embyprops = utils.window('Emby.nodes.total')
|
||||||
|
propnames = [
|
||||||
|
|
||||||
|
"index","path","title","content",
|
||||||
|
"inprogress.content","inprogress.title",
|
||||||
|
"inprogress.content","inprogress.path",
|
||||||
|
"nextepisodes.title","nextepisodes.content",
|
||||||
|
"nextepisodes.path","unwatched.title",
|
||||||
|
"unwatched.content","unwatched.path",
|
||||||
|
"recent.title","recent.content","recent.path",
|
||||||
|
"recentepisodes.title","recentepisodes.content",
|
||||||
|
"recentepisodes.path","inprogressepisodes.title",
|
||||||
|
"inprogressepisodes.content","inprogressepisodes.path"
|
||||||
|
]
|
||||||
|
|
||||||
|
if embyprops:
|
||||||
|
totalnodes = int(embyprops)
|
||||||
|
for i in range(totalnodes):
|
||||||
|
for prop in propnames:
|
||||||
|
utils.window('Emby.nodes.%s.%s' % (str(i), prop), clear=True)
|
Loading…
Reference in a new issue