Overhaul Part 1

This commit is contained in:
tomkat83 2016-01-29 20:07:21 +01:00
parent 99895ec49f
commit 8912a0b601
16 changed files with 719 additions and 465 deletions

View File

@ -91,8 +91,7 @@ class Main:
folderid = params['folderid'][0]
modes[mode](itemid, folderid)
elif mode == "companion":
resume = params.get('resume', '')[0]
modes[mode](itemid, resume=resume)
modes[mode](itemid, params=sys.argv[2])
else:
modes[mode]()
else:

View File

@ -55,6 +55,8 @@ import re
import json
from urllib import urlencode, quote_plus
from PlexFunctions import PlexToKodiTimefactor
try:
import xml.etree.cElementTree as etree
except ImportError:
@ -198,9 +200,9 @@ class PlexAPI():
'avatar': avatar,
'token': token
}
utils.settings('plexLogin', value=username)
utils.settings('plexToken', value=token)
utils.settings('plexhome', value=home)
utils.settings('plexLogin', username)
utils.settings('plexToken', token)
utils.settings('plexhome', home)
return result
def CheckPlexTvSignin(self, identifier):
@ -339,8 +341,8 @@ class PlexAPI():
verify=sslverify,
timeout=timeout)
except requests.exceptions.ConnectionError as e:
self.logMsg("Server is offline or cannot be reached. Url: %s."
"Header: %s. Error message: %s"
self.logMsg("Server is offline or cannot be reached. Url: %s "
"Header: %s Error message: %s"
% (url, header, e), -1)
return False
except requests.exceptions.ReadTimeout:
@ -781,8 +783,8 @@ class PlexAPI():
"""
# Get addon infos
xargs = {
"Content-type": "application/x-www-form-urlencoded",
"Access-Control-Allow-Origin": "*",
"Content-Type": "application/x-www-form-urlencoded",
# "Access-Control-Allow-Origin": "*",
'X-Plex-Language': 'en',
'X-Plex-Device': self.addonName,
'X-Plex-Client-Platform': self.platform,
@ -1371,8 +1373,6 @@ class API():
def __init__(self, item):
self.item = item
# which child in the XML response shall we look at?
self.child = 0
# which media part in the XML response shall we look at?
self.part = 0
self.clientinfo = clientinfo.ClientInfo()
@ -1383,18 +1383,6 @@ class API():
self.jumpback = int(utils.settings('resumeJumpBack'))
def setChildNumber(self, number=0):
"""
Which child in the XML response shall we look at and work with?
"""
self.child = int(number)
def getChildNumber(self):
"""
Returns the child in the XML response that we're currently looking at
"""
return self.child
def setPartNumber(self, number=0):
"""
Sets the part number to work with (used to deal with Movie with several
@ -1435,12 +1423,9 @@ class API():
def getType(self):
"""
Returns the type of media, e.g. 'movie'
Returns the type of media, e.g. 'movie' or 'clip' for trailers
"""
item = self.item
item = item[self.child].attrib
itemtype = item['type']
return itemtype
return self.item['type']
def getChecksum(self):
"""
@ -1450,47 +1435,58 @@ class API():
item = self.item
# XML
try:
item = item[self.child].attrib
item = item[0].attrib
# JSON
except KeyError:
except (AttributeError, KeyError):
pass
# Include a letter to prohibit saving as an int!
checksum = "K%s%s" % (self.getKey(),
checksum = "K%s%s" % (self.getRatingKey(),
item.get('updatedAt', ''))
return checksum
def getKey(self):
def getRatingKey(self):
"""
Can be used on both XML and JSON
Returns the Plex unique movie id as a str, not int
Returns the Plex key such as '246922' as a string
"""
item = self.item
# XML
try:
item = item[self.child].attrib
item = item[0].attrib
# JSON
except KeyError:
except (AttributeError, KeyError):
pass
key = item['ratingKey']
return str(key)
def getKey(self):
"""
Can be used on both XML and JSON
Returns the Plex key such as '/library/metadata/246922'
"""
item = self.item
# XML
try:
item = item[0].attrib
# JSON
except (AttributeError, KeyError):
pass
key = item['key']
return str(key)
def getIndex(self):
"""
Returns the 'index' of an PMS XML reply. Depicts e.g. season number.
"""
item = self.item[self.child].attrib
item = self.item[0].attrib
index = item['index']
return str(index)
def getDateCreated(self):
"""
Returns the date when this library item was created
Input:
index child number as int; normally =0
"""
item = self.item
item = item[self.child].attrib
item = self.item[0].attrib
dateadded = item['addedAt']
dateadded = self.convert_date(dateadded)
return dateadded
@ -1517,7 +1513,13 @@ class API():
resume = 0
rating = 0
item = item[self.child].attrib
# XML
try:
item = item[0].attrib
# JSON
except (AttributeError, KeyError):
pass
try:
playcount = int(item['viewCount'])
except KeyError:
@ -1558,7 +1560,7 @@ class API():
writer = []
cast = []
producer = []
for child in item[self.child]:
for child in item[0]:
if child.tag == 'Director':
director.append(child.attrib['tag'])
elif child.tag == 'Writer':
@ -1594,7 +1596,7 @@ class API():
'Role': 'Actor',
'Producer': 'Producer'
}
for child in item[self.child]:
for child in item[0]:
if child.tag in people_of_interest.keys():
name = child.attrib['tag']
name_id = child.attrib['id']
@ -1626,7 +1628,7 @@ class API():
"""
item = self.item
genre = []
for child in item[self.child]:
for child in item[0]:
if child.tag == 'Genre':
genre.append(child.attrib['tag'])
return genre
@ -1638,7 +1640,7 @@ class API():
Return IMDB, e.g. "imdb://tt0903624?lang=en". Returns None if not found
"""
item = self.item
item = item[self.child].attrib
item = item[0].attrib
try:
item = item['guid']
except KeyError:
@ -1663,12 +1665,14 @@ class API():
sorttitle = title, if no sorttitle is found
"""
item = self.item
# XML
try:
item = item[self.child].attrib
item = item[0].attrib
# JSON
except KeyError:
except (AttributeError, KeyError):
pass
try:
title = item['title']
except:
@ -1684,7 +1688,7 @@ class API():
Returns the plot or None.
"""
item = self.item
item = item[self.child].attrib
item = item[0].attrib
try:
plot = item['summary']
except:
@ -1696,7 +1700,7 @@ class API():
Returns a shorter tagline or None
"""
item = self.item
item = item[self.child].attrib
item = item[0].attrib
try:
tagline = item['tagline']
except KeyError:
@ -1708,7 +1712,7 @@ class API():
Returns the audience rating or None
"""
item = self.item
item = item[self.child].attrib
item = item[0].attrib
try:
rating = item['audienceRating']
except KeyError:
@ -1720,7 +1724,7 @@ class API():
Returns the production(?) year ("year") or None
"""
item = self.item
item = item[self.child].attrib
item = item[0].attrib
try:
year = item['year']
except KeyError:
@ -1737,9 +1741,14 @@ class API():
Output:
resume, runtime as floats. 0.0 if not found
"""
time_factor = 1.0 / 1000.0 # millisecond -> seconds
item = self.item
item = item[self.child].attrib
time_factor = PlexToKodiTimefactor()
# XML
try:
item = self.item[0].attrib
# JSON
except (AttributeError, KeyError):
pass
try:
runtime = float(item['duration'])
@ -1769,7 +1778,7 @@ class API():
"""
# Convert more complex cases
item = self.item
item = item[self.child].attrib
item = item[0].attrib
try:
mpaa = item['contentRating']
except KeyError:
@ -1785,7 +1794,7 @@ class API():
"""
item = self.item
country = []
for child in item[self.child]:
for child in item[0]:
if child.tag == 'Country':
country.append(child.attrib['tag'])
return country
@ -1795,7 +1804,7 @@ class API():
Returns the "originallyAvailableAt" or None
"""
item = self.item
item = item[self.child].attrib
item = item[0].attrib
try:
premiere = item['originallyAvailableAt']
except:
@ -1808,7 +1817,7 @@ class API():
"""
item = self.item
studio = []
item = item[self.child].attrib
item = item[0].attrib
try:
studio.append(self.getStudio(item['studio']))
except KeyError:
@ -1849,67 +1858,36 @@ class API():
Episode number, Plex: 'index'
]
"""
item = self.item[self.child].attrib
item = self.item[0].attrib
key = item['grandparentRatingKey']
title = item['grandparentTitle']
season = item['parentIndex']
episode = item['index']
return key, title, season, episode
def getFilePath(self):
"""
returns the path to the Plex object, e.g. "/library/metadata/221803"
"""
item = self.item
item = item[self.child].attrib
try:
filepath = item['key']
except KeyError:
filepath = ""
# Plex: do we need this?
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
return str(key), title, str(season), str(episode)
def addPlexCredentialsToUrl(self, url, arguments={}):
"""
Takes an URL and optional arguments (also to be URL-encoded); returns
an extended URL with e.g. the Plex token included.
arguments overrule everything
"""
token = {'X-Plex-Token': self.token}
xargs = PlexAPI().getXArgsDeviceInfo(options=token)
xargs.update(arguments)
url = "%s?%s" % (url, urlencode(xargs))
if '?' not in url:
url = "%s?%s" % (url, urlencode(xargs))
else:
url = "%s&%s" % (url, urlencode(xargs))
return url
def getBitrate(self):
def GetPlayQueueItemID(self):
"""
Returns the bitrate as an int. The Part bitrate is returned; if not
available in the Plex XML, the Media bitrate is returned
Returns current playQueueItemID for the item.
If not found, empty str is returned
"""
item = self.item
try:
bitrate = item[self.child][0][self.part].attrib['bitrate']
except KeyError:
bitrate = item[self.child][0].attrib['bitrate']
bitrate = int(bitrate)
return bitrate
return self.item.get('playQueueItemID')
def getDataFromPartOrMedia(self, key):
"""
@ -1918,8 +1896,8 @@ class API():
If all fails, None is returned.
"""
media = self.item[self.child][0].attrib
part = self.item[self.child][0][self.part].attrib
media = self.item['_children'][0]
part = media['_children'][self.part]
try:
try:
value = part[key]
@ -2025,12 +2003,12 @@ class API():
subtitlelanguages = []
aspectratio = None
try:
aspectratio = item[self.child][0].attrib['aspectRatio']
aspectratio = item[0][0].attrib['aspectRatio']
except KeyError:
pass
# TODO: what if several Media tags exist?!?
# Loop over parts
for child in item[self.child][0]:
for child in item[0][0]:
container = child.attrib['container'].lower()
# Loop over Streams
for grandchild in child:
@ -2105,6 +2083,13 @@ class API():
server = self.server
item = self.item
# XML
try:
item = item[0].attrib
# JSON
except (AttributeError, KeyError):
pass
maxHeight = 10000
maxWidth = 10000
customquery = ""
@ -2126,7 +2111,6 @@ class API():
}
# Process backdrops
# Get background artwork URL
item = item[self.child].attrib
try:
background = item['art']
background = "%s%s" % (server, background)
@ -2259,7 +2243,7 @@ class API():
xargs = PlexAPI().getXArgsDeviceInfo(options=options)
# For Direct Playing
if action == "DirectPlay":
path = self.item[self.child][0][self.part].attrib['key']
path = self.item['_children'][0]['_children'][self.partNumber]['key']
transcodePath = self.server + path
# Be sure to have exactly ONE '?' in the path (might already have
# been returned, e.g. trailers!)
@ -2273,15 +2257,7 @@ class API():
# For Direct Streaming or Transcoding
transcodePath = self.server + \
'/video/:/transcode/universal/start.m3u8?'
partCount = 0
for parts in self.item[self.child][0]:
partCount = partCount + 1
# Movie consists of several parts; grap one part
if partCount > 1:
path = self.item[self.child][0][self.part].attrib['key']
# Movie consists of only one part
else:
path = self.item[self.child].attrib['key']
path = self.getDataFromPartOrMedia('key')
args = {
'path': path,
'mediaIndex': 0, # Probably refering to XML reply sheme
@ -2337,9 +2313,9 @@ class API():
mapping = {}
item = self.item
itemid = self.getKey()
itemid = self.getRatingKey()
try:
mediastreams = item[self.child][0][0]
mediastreams = item[0][0][0]
except (TypeError, KeyError, IndexError):
return
@ -2372,14 +2348,14 @@ class API():
Returns raw API metadata XML dump for a playlist with e.g. trailers.
"""
item = self.item
key = self.getKey()
key = self.getRatingKey()
try:
uuid = item.attrib['librarySectionUUID']
# if not found: probably trying to start a trailer directly
# Hence no playlist needed
except KeyError:
return None
mediatype = item[self.child].tag.lower()
mediatype = item[0].tag.lower()
trailerNumber = utils.settings('trailerNumber')
if not trailerNumber:
trailerNumber = '3'
@ -2407,4 +2383,4 @@ class API():
"""
Returns the parts of the specified video child in the XML response
"""
return self.item[self.child][0]
return self.item[0][0]

View File

@ -1,9 +1,12 @@
# -*- coding: utf-8 -*-
from urllib import urlencode
from ast import literal_eval
from urlparse import urlparse, parse_qs
import re
from xbmcaddon import Addon
from downloadutils import DownloadUtils
import downloadutils
from utils import logMsg
@ -11,6 +14,13 @@ addonName = Addon().getAddonInfo('name')
title = "%s %s" % (addonName, __name__)
def PlexToKodiTimefactor():
"""
Kodi measures time in seconds, but Plex in milliseconds
"""
return 1.0 / 1000.0
def GetItemClassFromType(itemType):
classes = {
'movie': 'Movies',
@ -21,6 +31,42 @@ def GetItemClassFromType(itemType):
return classes[itemType]
def GetPlexKeyNumber(plexKey):
"""
Deconstructs e.g. '/library/metadata/xxxx' to the tuple
('library/metadata', 'xxxx')
Returns ('','') if nothing is found
"""
regex = re.compile(r'''/(.+)/(\d+)$''')
try:
result = regex.findall(plexKey)[0]
except IndexError:
result = ('', '')
return result
def ParseContainerKey(containerKey):
"""
Parses e.g. /playQueues/3045?own=1&repeat=0&window=200 to:
'playQueues', '3045', {'window': ['200'], 'own': ['1'], 'repeat': ['0']}
Output hence: library, key, query (query as a special dict)
"""
result = urlparse(containerKey)
library, key = GetPlexKeyNumber(result.path)
query = parse_qs(result.query)
return library, key, query
def LiteralEval(string):
"""
Turns a string e.g. in a dict, safely :-)
"""
return literal_eval(string)
def GetMethodFromPlexType(plexType):
methods = {
'movie': 'add_update',
@ -46,18 +92,25 @@ def EmbyItemtypes():
return ['Movie', 'Series', 'Season', 'Episode']
def XbmcPhoto():
return "photo"
def XbmcVideo():
return "video"
def XbmcAudio():
return "audio"
def PlexPhoto():
return "photo"
def PlexVideo():
return "video"
def PlexAudio():
return "music"
def GetPlayQueue(playQueueID):
"""
Fetches the PMS playqueue with the playQueueID as a JSON
Returns False if something went wrong
"""
url = "{server}/playQueues/%s" % playQueueID
headerOptions = {'Accept': 'application/json'}
json = downloadutils.DownloadUtils().downloadUrl(url, headerOptions=headerOptions)
try:
json = json.json()
except:
return False
try:
json['_children']
json['playQueueID']
except KeyError:
return False
return json
def GetPlexMetadata(key):
@ -87,7 +140,7 @@ def GetPlexMetadata(key):
}
url = url + '?' + urlencode(arguments)
headerOptions = {'Accept': 'application/xml'}
xml = DownloadUtils().downloadUrl(url, headerOptions=headerOptions)
xml = downloadutils.DownloadUtils().downloadUrl(url, headerOptions=headerOptions)
# Did we receive a valid XML?
try:
xml.tag
@ -108,7 +161,7 @@ def GetAllPlexChildren(key):
"""
result = []
url = "{server}/library/metadata/%s/children" % key
jsondata = DownloadUtils().downloadUrl(url)
jsondata = downloadutils.DownloadUtils().downloadUrl(url)
try:
result = jsondata['_children']
except KeyError:
@ -125,7 +178,7 @@ def GetPlexSectionResults(viewId, headerOptions={}):
"""
result = []
url = "{server}/library/sections/%s/all" % viewId
jsondata = DownloadUtils().downloadUrl(url, headerOptions=headerOptions)
jsondata = downloadutils.DownloadUtils().downloadUrl(url, headerOptions=headerOptions)
try:
result = jsondata['_children']
except TypeError:
@ -146,37 +199,38 @@ def GetPlexSectionResults(viewId, headerOptions={}):
return result
def GetPlexUpdatedItems(viewId, unixTime, headerOptions={}):
"""
Returns a list (raw JSON or XML API dump) of all Plex items in the Plex
section with key = viewId AFTER the unixTime
"""
result = []
url = "{server}/library/sections/%s/allLeaves?updatedAt>=%s" \
% (viewId, unixTime)
jsondata = DownloadUtils().downloadUrl(url, headerOptions=headerOptions)
try:
result = jsondata['_children']
except KeyError:
logMsg(title,
"Error retrieving all items for Plex section %s and time %s"
% (viewId, unixTime), -1)
return result
def GetAllPlexLeaves(viewId, headerOptions={}):
def GetAllPlexLeaves(viewId, lastViewedAt=None, updatedAt=None,
headerOptions={}):
"""
Returns a list (raw JSON or XML API dump) of all Plex subitems for the
key.
(e.g. /library/sections/2/allLeaves pointing to all TV shows)
Input:
viewId Id of Plex library, e.g. '2'
headerOptions to override the download headers
viewId Id of Plex library, e.g. '2'
lastViewedAt Unix timestamp; only retrieves PMS items viewed
since that point of time until now.
updatedAt Unix timestamp; only retrieves PMS items updated
by the PMS since that point of time until now.
headerOptions to override any download headers
If lastViewedAt and updatedAt=None, ALL PMS items are returned.
Warning: lastViewedAt and updatedAt are combined with AND by the PMS!
Relevant "master time": PMS server. I guess this COULD lead to problems,
e.g. when server and client are in different time zones.
"""
result = []
url = "{server}/library/sections/%s/allLeaves" % viewId
jsondata = DownloadUtils().downloadUrl(url, headerOptions=headerOptions)
args = []
url = "{server}/library/sections/%s/allLeaves?" % viewId
if lastViewedAt:
args.append('lastViewedAt>=%s' % lastViewedAt)
if updatedAt:
args.append('updatedAt>=%s' % updatedAt)
args = '&'.join(args)
jsondata = downloadutils.DownloadUtils().downloadUrl(
url+args, headerOptions=headerOptions)
try:
result = jsondata['_children']
except TypeError:
@ -213,7 +267,7 @@ def GetPlexCollections(mediatype):
"""
collections = []
url = "{server}/library/sections"
jsondata = DownloadUtils().downloadUrl(url)
jsondata = downloadutils.DownloadUtils().downloadUrl(url)
try:
result = jsondata['_children']
except KeyError:

View File

@ -31,36 +31,65 @@ import embydb_functions
#################################################################################################
# For logging only
title = " %s %s" % (clientinfo.ClientInfo().getAddonName(), __name__)
def plexCompanion(fullurl, params=None):
params = PlexFunctions.LiteralEval(params[26:])
utils.logMsg("entrypoint - plexCompanion",
"params is: %s" % params, -1)
# {'protocol': 'http',
# 'containerKey': '/playQueues/3045?own=1&repeat=0&window=200',
# 'offset': '0',
# 'commandID': '20',
# 'token': 'transient-0243a39f-4c7d-495f-a5c8-6991b622b5a6',
# 'key': '/library/metadata/470',
# 'address': '192.168.0.2',
# 'machineIdentifier': '3eb2fc28af89500e000db2e07f8e8234d159f2c4',
# 'type': 'video',
# 'port': '32400'}
if (params.get('machineIdentifier') !=
utils.window('plex_machineIdentifier')):
utils.logMsg(
title,
"Command was not for us, machineIdentifier controller: %s, "
"our machineIdentifier : %s"
% (params.get('machineIdentifier'),
utils.window('plex_machineIdentifier')), -1)
return
utils.window('plex_key', params.get('key'))
library, key, query = PlexFunctions(params.get('containerKey'))
# Construct a container key that works always (get rid of playlist args)
utils.window('plex_containerKey', '/'+library+'/'+key)
# Assume it's video when something goes wrong
playbackType = params.get('type', 'video')
if 'playQueues' in library:
utils.logMsg(title, "Playing a playQueue. Query was: %s" % query, 1)
# Playing a playlist that we need to fetch from PMS
playQueue = PlexFunctions.GetPlayQueue(key)
if not playQueue:
utils.logMsg(
title, "Error getting PMS playlist for key %s" % key, -1)
return
# Set window properties to make them available for other threads
utils.window('plex_playQueueID', playQueue['playQueueID'])
utils.window('plex_playQueueVersion', playQueue['playQueueVersion'])
utils.window('plex_playQueueShuffled', playQueue['playQueueShuffled'])
utils.window(
'plex_playQueueSelectedItemID',
playQueue['playQueueSelectedItemID'])
utils.window(
'plex_playQueueSelectedItemOffset',
playQueue['playQueueSelectedItemOffset'])
pbutils.PlaybackUtils(playQueue['_children']).StartPlay(
resume=playQueue['playQueueSelectedItemOffset'],
resumeItem=playQueue['playQueueSelectedItemID'])
def plexCompanion(fullurl, resume=None):
regex = re.compile(r'''/(\d+)$''')
itemid = regex.findall(fullurl)
try:
itemid = itemid[0]
except IndexError:
# No matches found, url not like:
# http://192.168.0.2:32400/library/metadata/243480
utils.logMsg("entrypoint - plexCompanion",
"Could not parse url: %s" % fullurl, -1)
return False
# Initialize embydb
embyconn = utils.kodiSQL('emby')
embycursor = embyconn.cursor()
emby = embydb_functions.Embydb_Functions(embycursor)
# Get dbid using itemid
# Works only for library items, not e.g. for trailers
try:
dbid = emby.getItem_byId(itemid)[0]
except TypeError:
# Trailers and the like
dbid = None
embyconn.close()
# Fix resume timing
if resume:
if resume == '0':
resume = None
else:
resume = round(float(resume) / 1000.0, 6)
# Start playing
item = PlexFunctions.GetPlexMetadata(itemid)
pbutils.PlaybackUtils(item).play(itemid, dbid, seektime=resume)

View File

@ -42,6 +42,7 @@ class InitialSetup():
clientId = self.clientInfo.getDeviceId()
serverid = self.userClient.getServerId()
myplexlogin, plexhome, plexLogin, plexToken = self.plx.GetPlexLoginFromSettings()
dialog = xbmcgui.Dialog()
# Optionally sign into plex.tv. Will not be called on very first run
# as plexToken will be ''
@ -49,7 +50,6 @@ class InitialSetup():
chk = self.plx.CheckConnection('plex.tv', plexToken)
# HTTP Error: unauthorized
if chk == 401:
dialog = xbmcgui.Dialog()
dialog.ok(
self.addonName,
'Could not login to plex.tv.',
@ -60,7 +60,6 @@ class InitialSetup():
plexLogin = result['username']
plexToken = result['token']
elif chk is False or chk >= 400:
dialog = xbmcgui.Dialog()
dialog.ok(
self.addonName,
'Problems connecting to plex.tv.',
@ -81,12 +80,8 @@ class InitialSetup():
plexLogin = result['username']
plexToken = result['token']
# Get g_PMS list of servers (saved to plx.g_PMS)
serverNum = 1
while serverNum > 0:
if plexToken:
tokenDict = {'MyPlexToken': plexToken}
else:
tokenDict = {}
while True:
tokenDict = {'MyPlexToken': plexToken} if plexToken else {}
# Populate g_PMS variable with the found Plex servers
self.plx.discoverPMS(clientId,
None,
@ -100,8 +95,12 @@ class InitialSetup():
# Get a nicer list
dialoglist = []
# Exit if no servers found
serverNum = len(serverlist)
if serverNum == 0:
if len(serverlist) == 0:
dialog.ok(
self.addonName,
'Could not find any Plex server in the network.'
'Aborting...'
)
break
for server in serverlist:
if server['local'] == '1':
@ -109,7 +108,6 @@ class InitialSetup():
dialoglist.append(str(server['name']) + ' (nearby)')
else:
dialoglist.append(str(server['name']))
dialog = xbmcgui.Dialog()
resp = dialog.select(
'Choose your Plex server',
dialoglist)
@ -119,11 +117,11 @@ class InitialSetup():
server['port']
# Deactive SSL verification if the server is local!
if server['local'] == '1':
self.addon.setSetting('sslverify', 'false')
utils.settings('sslverify', 'false')
self.logMsg("Setting SSL verify to false, because server is "
"local", 1)
else:
self.addon.setSetting('sslverify', 'true')
utils.settings('sslverify', 'true')
self.logMsg("Setting SSL verify to true, because server is "
"not local", 1)
chk = self.plx.CheckConnection(url, server['accesstoken'])
@ -142,7 +140,6 @@ class InitialSetup():
break
# Problems connecting
elif chk >= 400 or chk is False:
dialog = xbmcgui.Dialog()
resp = dialog.yesno(self.addonName,
'Problems connecting to server.',
'Pick another server?')
@ -158,20 +155,19 @@ class InitialSetup():
xbmc.executebuiltin('Addon.OpenSettings(%s)' % self.addonId)
return
# Write to Kodi settings file
self.addon.setSetting('plex_machineIdentifier', activeServer)
self.addon.setSetting('ipaddress', server['ip'])
self.addon.setSetting('port', server['port'])
utils.settings('plex_machineIdentifier', activeServer)
utils.settings('ipaddress', server['ip'])
utils.settings('port', server['port'])
if server['scheme'] == 'https':
self.addon.setSetting('https', 'true')
utils.settings('https', 'true')
else:
self.addon.setSetting('https', 'false')
utils.settings('https', 'false')
self.logMsg("Wrote to Kodi user settings file:", 0)
self.logMsg("PMS machineIdentifier: %s, ip: %s, port: %s, https: %s "
% (activeServer, server['ip'], server['port'],
server['scheme']), 0)
##### ADDITIONAL PROMPTS #####
dialog = xbmcgui.Dialog()
directPaths = dialog.yesno(
heading="%s: Playback Mode" % self.addonName,
line1=(

View File

@ -263,7 +263,7 @@ class Movies(Items):
API = PlexAPI.API(itemList)
for itemNumber in range(len(itemList)):
API.setChildNumber(itemNumber)
itemid = API.getKey()
itemid = API.getRatingKey()
# Get key and db entry on the Kodi db side
fileid = self.emby_db.getItem_byId(itemid)[1]
# Grab the user's viewcount, resume points etc. from PMS' answer
@ -276,6 +276,7 @@ class Movies(Items):
userdata['LastPlayedDate'])
def add_update(self, item, viewtag=None, viewid=None):
self.logMsg("Entering add_update", 1)
# Process single movie
kodicursor = self.kodicursor
emby_db = self.emby_db
@ -286,7 +287,7 @@ class Movies(Items):
# If the item already exist in the local Kodi DB we'll perform a full item update
# If the item doesn't exist, we'll add it to the database
update_item = True
itemid = API.getKey()
itemid = API.getRatingKey()
# Cannot parse XML, abort
if not itemid:
self.logMsg("Cannot parse XML data for movie", -1)
@ -346,22 +347,22 @@ class Movies(Items):
# Find one trailer
trailer = None
extras = API.getExtras()
for item in extras:
for extra in extras:
# Only get 1st trailer element
if item['extraType'] == '1':
trailer = item['key']
if extra['extraType'] == '1':
trailer = extra['key']
trailer = "plugin://plugin.video.plexkodiconnect/trailer/?id=%s&mode=play" % trailer
self.logMsg("Trailer for %s: %s" % (itemid, trailer), 2)
break
##### GET THE FILE AND PATH #####
playurl = API.getFilePath()
if "\\" in playurl:
# Local path
filename = playurl.rsplit("\\", 1)[1]
else: # Network share
filename = playurl.rsplit("/", 1)[1]
playurl = API.getKey()
filename = playurl
# if "\\" in playurl:
# # Local path
# filename = playurl.rsplit("\\", 1)[1]
# else: # Network share
# filename = playurl.rsplit("/", 1)[1]
if self.directpath:
# Direct paths is set the Kodi way
@ -386,13 +387,13 @@ class Movies(Items):
path = "plugin://plugin.video.plexkodiconnect.movies/"
params = {
'filename': filename.encode('utf-8'),
#'filename': filename.encode('utf-8'),
'filename': filename,
'id': itemid,
'dbid': movieid,
'mode': "play"
}
filename = "%s?%s" % (path, urllib.urlencode(params))
##### UPDATE THE MOVIE #####
if update_item:
self.logMsg("UPDATE movie itemid: %s - Title: %s" % (itemid, title), 1)
@ -462,24 +463,33 @@ class Movies(Items):
# Process cast
people = API.getPeopleList()
kodi_db.addPeople(movieid, people, "movie")
self.logMsg('People added', 2)
# Process genres
kodi_db.addGenres(movieid, genres, "movie")
self.logMsg('Genres added', 2)
# Process artwork
allartworks = API.getAllArtwork()
self.logMsg('Artwork processed', 2)
artwork.addArtwork(allartworks, movieid, "movie", kodicursor)
self.logMsg('Artwork added', 2)
# Process stream details
streams = API.getMediaStreams()
self.logMsg('Streames processed', 2)
kodi_db.addStreams(fileid, streams, runtime)
self.logMsg('Streames added', 2)
# Process studios
kodi_db.addStudios(movieid, studios, "movie")
self.logMsg('Studios added', 2)
# Process tags: view, emby tags
tags = [viewtag]
# tags.extend(item['Tags'])
# if userdata['Favorite']:
# tags.append("Favorite movies")
kodi_db.addTags(movieid, tags, "movie")
self.logMsg('Tags added', 2)
# Process playstates
kodi_db.addPlaystate(fileid, resume, runtime, playcount, dateplayed)
self.logMsg('Done processing %s' % itemid, 2)
def remove(self, itemid):
# Remove movieid, fileid, emby reference
@ -598,7 +608,7 @@ class MusicVideos(Items):
##### GET THE FILE AND PATH #####
playurl = API.getFilePath()
playurl = API.getKey()
if "\\" in playurl:
# Local path
@ -883,7 +893,7 @@ class TVShows(Items):
API = PlexAPI.API(itemList)
for itemNumber in range(len(itemList)):
API.setChildNumber(itemNumber)
itemid = API.getKey()
itemid = API.getRatingKey()
# Get key and db entry on the Kodi db side
fileid = self.emby_db.getItem_byId(itemid)[1]
# Grab the user's viewcount, resume points etc. from PMS' answer
@ -909,7 +919,7 @@ class TVShows(Items):
# If the item already exist in the local Kodi DB we'll perform a full item update
# If the item doesn't exist, we'll add it to the database
update_item = True
itemid = API.getKey()
itemid = API.getRatingKey()
if not itemid:
self.logMsg("Cannot parse XML data for TV show", -1)
return
@ -943,7 +953,7 @@ class TVShows(Items):
studio = None
##### GET THE FILE AND PATH #####
playurl = API.getFilePath()
playurl = API.getKey()
if self.directpath:
# Direct paths is set the Kodi way
@ -1070,7 +1080,7 @@ class TVShows(Items):
def add_updateSeason(self, item, viewid=None, viewtag=None):
API = PlexAPI.API(item)
showid = viewid
itemid = API.getKey()
itemid = API.getRatingKey()
kodicursor = self.kodicursor
emby_db = self.emby_db
kodi_db = self.kodi_db
@ -1116,7 +1126,7 @@ class TVShows(Items):
# If the item already exist in the local Kodi DB we'll perform a full item update
# If the item doesn't exist, we'll add it to the database
update_item = True
itemid = API.getKey()
itemid = API.getRatingKey()
emby_dbitem = emby_db.getItem_byId(itemid)
self.logMsg("Processing episode with Plex Id: %s" % itemid, 2)
try:
@ -1150,8 +1160,12 @@ class TVShows(Items):
resume, runtime = API.getRuntime()
premieredate = API.getPremiereDate()
self.logMsg("Retrieved metadata for %s" % itemid, 2)
# episode details
seriesId, seriesName, season, episode = API.getEpisodeDetails()
self.logMsg("Got episode details: %s %s: s%se%s"
% (seriesId, seriesName, season, episode), 2)
if season is None:
if item.get('AbsoluteEpisodeNumber'):
@ -1180,27 +1194,30 @@ class TVShows(Items):
try:
showid = show[0]
except TypeError:
# Show is missing from database
show = self.emby.getItem(seriesId)
self.add_update(show)
show = emby_db.getItem_byId(seriesId)
try:
showid = show[0]
except TypeError:
self.logMsg("Skipping: %s. Unable to add series: %s." % (itemid, seriesId), -1)
return False
# self.logMsg("Show is missing from database, trying to add", 2)
# show = self.emby.getItem(seriesId)
# self.logMsg("Show now: %s. Trying to add new show" % show, 2)
# self.add_update(show)
# show = emby_db.getItem_byId(seriesId)
# try:
# showid = show[0]
# except TypeError:
# self.logMsg("Skipping: %s. Unable to add series: %s." % (itemid, seriesId), -1)
self.logMsg("Parent tvshow now found, skip item", 2)
return False
self.logMsg("showid: %s" % showid, 2)
seasonid = kodi_db.addSeason(showid, season)
self.logMsg("seasonid: %s" % seasonid, 2)
##### GET THE FILE AND PATH #####
playurl = API.getFilePath()
playurl = API.getKey()
filename = playurl
if "\\" in playurl:
# Local path
filename = playurl.rsplit("\\", 1)[1]
else: # Network share
filename = playurl.rsplit("/", 1)[1]
# if "\\" in playurl:
# # Local path
# filename = playurl.rsplit("\\", 1)[1]
# else: # Network share
# filename = playurl.rsplit("/", 1)[1]
if self.directpath:
# Direct paths is set the Kodi way
@ -1225,7 +1242,8 @@ class TVShows(Items):
path = "plugin://plugin.video.plexkodiconnect.tvshows/%s/" % seriesId
params = {
'filename': filename.encode('utf-8'),
#'filename': filename.encode('utf-8'),
'filename': filename,
'id': itemid,
'dbid': episodeid,
'mode': "play"
@ -1234,7 +1252,7 @@ class TVShows(Items):
##### UPDATE THE EPISODE #####
if update_item:
self.logMsg("UPDATE episode itemid: %s - Title: %s" % (itemid, title), 1)
self.logMsg("UPDATE episode itemid: %s" % (itemid), 1)
# Update the movie entry
if kodiversion in (16, 17):
@ -1268,7 +1286,7 @@ class TVShows(Items):
##### OR ADD THE EPISODE #####
else:
self.logMsg("ADD episode itemid: %s - Title: %s" % (itemid, title), 1)
self.logMsg("ADD episode itemid: %s" % (itemid), 1)
# Add path
pathid = kodi_db.addPath(path)
@ -1850,7 +1868,7 @@ class Music(Items):
path = "%s/emby/Audio/%s/" % (self.server, itemid)
filename = "stream.mp3"
else:
playurl = API.getFilePath()
playurl = API.getKey()
if "\\" in playurl:
# Local path

View File

@ -27,7 +27,7 @@ import PlexFunctions
##################################################################################################
@utils.ThreadMethodsStopsync
@utils.ThreadMethodsAdditionalStop('emby_shouldStop')
@utils.ThreadMethods
class ThreadedGetMetadata(threading.Thread):
"""
@ -41,10 +41,11 @@ class ThreadedGetMetadata(threading.Thread):
the downloaded metadata XMLs as etree objects
lock threading.Lock(), used for counting where we are
"""
def __init__(self, queue, out_queue, lock):
def __init__(self, queue, out_queue, lock, errorQueue):
self.queue = queue
self.out_queue = out_queue
self.lock = lock
self.errorQueue = errorQueue
threading.Thread.__init__(self)
def run(self):
@ -54,35 +55,41 @@ class ThreadedGetMetadata(threading.Thread):
lock = self.lock
threadStopped = self.threadStopped
global getMetadataCount
while threadStopped() is False:
# grabs Plex item from queue
try:
updateItem = queue.get(block=False)
# Empty queue
except Queue.Empty:
continue
# Download Metadata
try:
try:
while threadStopped() is False:
# grabs Plex item from queue
try:
updateItem = queue.get(block=False)
# Empty queue
except Queue.Empty:
continue
# Download Metadata
plexXML = PlexFunctions.GetPlexMetadata(updateItem['itemId'])
except:
raise
# check whether valid XML
if plexXML:
try:
plexXML.tag
except:
# Did not receive a valid XML - skip that one for now
queue.task_done()
continue
# Get rid of first XML level:
updateItem['XML'] = plexXML
# place item into out queue
out_queue.put(updateItem)
del plexXML
del updateItem
# If we don't have a valid XML, don't put that into the queue
# but skip this item for now
# Keep track of where we are at
with lock:
getMetadataCount += 1
# signals to queue job is done
queue.task_done()
del updateItem
# If we don't have a valid XML, don't put that into the queue
# but skip this item for now
# Keep track of where we are at
with lock:
getMetadataCount += 1
# signals to queue job is done
queue.task_done()
except:
self.errorQueue.put(sys.exc_info())
@utils.ThreadMethodsStopsync
@utils.ThreadMethodsAdditionalStop('emby_shouldStop')
@utils.ThreadMethods
class ThreadedProcessMetadata(threading.Thread):
"""
@ -96,10 +103,11 @@ class ThreadedProcessMetadata(threading.Thread):
e.g. 'Movies' => itemtypes.Movies()
lock: threading.Lock(), used for counting where we are
"""
def __init__(self, queue, itemType, lock):
def __init__(self, queue, itemType, lock, errorQueue):
self.queue = queue
self.lock = lock
self.itemType = itemType
self.errorQueue = errorQueue
threading.Thread.__init__(self)
def run(self):
@ -111,35 +119,40 @@ class ThreadedProcessMetadata(threading.Thread):
threadStopped = self.threadStopped
global processMetadataCount
global processingViewName
with itemFkt() as item:
while threadStopped() is False:
# grabs item from queue
try:
updateItem = queue.get(block=False)
# Empty queue
except Queue.Empty:
continue
# Do the work; lock to be sure we've only got 1 Thread
plexitem = updateItem['XML']
method = updateItem['method']
viewName = updateItem['viewName']
viewId = updateItem['viewId']
title = updateItem['title']
itemSubFkt = getattr(item, method)
with lock:
itemSubFkt(plexitem,
viewtag=viewName,
viewid=viewId)
# Keep track of where we are at
processMetadataCount += 1
processingViewName = title
del plexitem
del updateItem
# signals to queue job is done
self.queue.task_done()
try:
with itemFkt() as item:
while threadStopped() is False:
# grabs item from queue
try:
updateItem = queue.get(block=False)
# Empty queue
except Queue.Empty:
continue
# Do the work; lock to be sure we've only got 1 Thread
plexitem = updateItem['XML']
method = updateItem['method']
viewName = updateItem['viewName']
viewId = updateItem['viewId']
title = updateItem['title']
itemSubFkt = getattr(item, method)
with lock:
itemSubFkt(plexitem,
viewtag=viewName,
viewid=viewId)
# Keep track of where we are at
processMetadataCount += 1
processingViewName = title
del plexitem
del updateItem
# signals to queue job is done
self.queue.task_done()
except:
xbmc.log('An error occured')
xbmc.log(sys.exc_info())
self.errorQueue.put(sys.exc_info())
@utils.ThreadMethodsStopsync
@utils.ThreadMethodsAdditionalStop('emby_shouldStop')
@utils.ThreadMethods
class ThreadedShowSyncInfo(threading.Thread):
"""
@ -184,10 +197,15 @@ class ThreadedShowSyncInfo(threading.Thread):
percentage = int(float(totalProgress) / float(total)*100.0)
except ZeroDivisionError:
percentage = 0
dialog.update(percentage,
message="Downloaded: %s, Processed: %s: %s"
% (getMetadataProgress,
processMetadataProgress, viewName))
try:
dialog.update(
percentage,
message="Downloaded: %s, Processed: %s: %s"
% (getMetadataProgress, processMetadataProgress,
viewName))
except:
# Unicode formating of the string?!?
pass
# Sleep for x milliseconds
xbmc.sleep(500)
dialog.close()
@ -195,7 +213,7 @@ class ThreadedShowSyncInfo(threading.Thread):
@utils.logging
@utils.ThreadMethodsAdditionalSuspend('suspend_LibraryThread')
@utils.ThreadMethodsStopsync
@utils.ThreadMethodsAdditionalStop('emby_shouldStop')
@utils.ThreadMethods
class LibrarySync(threading.Thread):
@ -213,6 +231,9 @@ class LibrarySync(threading.Thread):
self.__dict__ = self._shared_state
# How long should we look into the past for fast syncing items (in s)
self.syncPast = 60
self.clientInfo = clientinfo.ClientInfo()
self.doUtils = downloadutils.DownloadUtils()
self.user = userclient.UserClient()
@ -257,13 +278,14 @@ class LibrarySync(threading.Thread):
"""
self.compare = True
# Get last sync time
lastSync = utils.window('LastIncrementalSync')
lastSync = self.lastSync - self.syncPast
if not lastSync:
# Original Emby format:
# lastSync = "2016-01-01T00:00:00Z"
# January 1, 2015 at midnight:
lastSync = '1420070400'
self.logMsg("Last sync run: %s" % lastSync, 1)
lastSync = 1420070400
# Set new timestamp NOW because sync might take a while
self.saveLastSync()
# Get all PMS items already saved in Kodi
embyconn = utils.kodiSQL('emby')
@ -287,7 +309,8 @@ class LibrarySync(threading.Thread):
if self.threadStopped():
return True
# Get items per view
items = PlexFunctions.GetPlexUpdatedItems(view['id'], lastSync)
items = PlexFunctions.GetAllPlexLeaves(
view['id'], updatedAt=lastSync)
if not items:
continue
# Get one itemtype, because they're the same in the PMS section
@ -311,18 +334,16 @@ class LibrarySync(threading.Thread):
for view in self.views:
self.PlexUpdateWatched(
view['id'],
PlexFunctions.GetItemClassFromType(view['itemtype']))
PlexFunctions.GetItemClassFromType(view['itemtype']),
lastViewedAt=lastSync)
# Reset and return
self.saveLastSync()
self.allKodiElementsId = {}
self.allPlexElementsId = {}
return True
def saveLastSync(self):
# Save last sync time
lastSync = str(utils.getUnixTimestamp())
self.logMsg("New sync time: %s" % lastSync, 1)
utils.window('LastIncrementalSync', value=lastSync)
self.lastSync = utils.getUnixTimestamp()
def initializeDBs(self):
"""
@ -348,7 +369,7 @@ class LibrarySync(threading.Thread):
def fullSync(self, manualrun=False, repair=False):
# Only run once when first setting up. Can be run manually.
self.compare = manualrun
self.compare = manualrun or repair
music_enabled = utils.settings('enableMusic') == "true"
# Add sources
@ -361,7 +382,8 @@ class LibrarySync(threading.Thread):
else:
message = "Initial sync"
utils.window('emby_initialScan', value="true")
# Set new timestamp NOW because sync might take a while
self.saveLastSync()
starttotal = datetime.now()
# Ensure that DBs exist if called for very first time
@ -418,7 +440,6 @@ class LibrarySync(threading.Thread):
# musiccursor.close()
xbmc.executebuiltin('UpdateLibrary(video)')
self.saveLastSync()
elapsedtotal = datetime.now() - starttotal
utils.window('emby_initialScan', clear=True)
@ -572,7 +593,7 @@ class LibrarySync(threading.Thread):
Output: self.updatelist, self.allPlexElementsId
self.updatelist APPENDED(!!) list itemids (Plex Keys as
as received from API.getKey())
as received from API.getRatingKey())
One item in this list is of the form:
'itemId': xxx,
'itemType': 'Movies','TVShows', ...
@ -594,7 +615,7 @@ class LibrarySync(threading.Thread):
return False
API = PlexAPI.API(item)
plex_checksum = API.getChecksum()
itemId = API.getKey()
itemId = API.getRatingKey()
title, sorttitle = API.getTitle()
self.allPlexElementsId[itemId] = plex_checksum
kodi_checksum = self.allKodiElementsId.get(itemId)
@ -616,7 +637,7 @@ class LibrarySync(threading.Thread):
if self.threadStopped():
return False
API = PlexAPI.API(item)
itemId = API.getKey()
itemId = API.getRatingKey()
title, sorttitle = API.getTitle()
plex_checksum = API.getChecksum()
self.allPlexElementsId[itemId] = plex_checksum
@ -648,6 +669,7 @@ class LibrarySync(threading.Thread):
self.logMsg("Starting sync threads", 1)
getMetadataQueue = Queue.Queue()
processMetadataQueue = Queue.Queue(maxsize=100)
errorQueue = Queue.Queue()
getMetadataLock = threading.Lock()
processMetadataLock = threading.Lock()
# To keep track
@ -665,20 +687,12 @@ class LibrarySync(threading.Thread):
for i in range(min(self.syncThreadNumber, itemNumber)):
thread = ThreadedGetMetadata(getMetadataQueue,
processMetadataQueue,
getMetadataLock)
getMetadataLock,
errorQueue)
thread.setDaemon(True)
thread.start()
threads.append(thread)
self.logMsg("Download threads spawned", 1)
# Spawn one more thread to process Metadata, once downloaded
thread = ThreadedProcessMetadata(processMetadataQueue,
itemType,
processMetadataLock)
thread.setDaemon(True)
thread.start()
threads.append(thread)
self.logMsg("Processing thread spawned", 1)
# Start one thread to show sync progress
dialog = xbmcgui.DialogProgressBG()
thread = ThreadedShowSyncInfo(dialog,
@ -689,9 +703,32 @@ class LibrarySync(threading.Thread):
thread.start()
threads.append(thread)
self.logMsg("Kodi Infobox thread spawned", 1)
# Spawn one more thread to process Metadata, once downloaded
thread = ThreadedProcessMetadata(processMetadataQueue,
itemType,
processMetadataLock,
errorQueue)
thread.setDaemon(True)
thread.start()
threads.append(thread)
self.logMsg("Processing thread spawned", 1)
# Wait until finished
getMetadataQueue.join()
processMetadataQueue.join()
while True:
try:
exc = errorQueue.get(block=False)
except Queue.Empty:
pass
else:
exc_type, exc_obj, exc_trace = exc
# deal with the exception
self.logMsg("Error occured in thread", -1)
self.logMsg(str(exc_type) + str(exc_obj), -1)
self.logMsg(exc_trace, -1)
if getMetadataQueue.empty() and processMetadataQueue.empty():
break
xbmc.sleep(500)
# Kill threads
self.logMsg("Waiting to kill threads", 1)
for thread in threads:
@ -770,24 +807,24 @@ class LibrarySync(threading.Thread):
self.logMsg("%s sync is finished." % itemType, 1)
return True
def PlexUpdateWatched(self, viewId, itemType):
def PlexUpdateWatched(self, viewId, itemType,
lastViewedAt=None, updatedAt=None):
"""
Updates ALL plex elements' view status ('watched' or 'unwatched') and
Updates plex elements' view status ('watched' or 'unwatched') and
also updates resume times.
This is done by downloading one XML for ALL elements with viewId
"""
starttotal = datetime.now()
# Download XML, not JSON, because PMS JSON seems to be damaged
headerOptions = {'Accept': 'application/xml'}
plexItems = PlexFunctions.GetAllPlexLeaves(
viewId, headerOptions=headerOptions)
itemMth = getattr(itemtypes, itemType)
with itemMth() as method:
method.updateUserdata(plexItems)
elapsedtotal = datetime.now() - starttotal
self.logMsg("Syncing userdata for itemtype %s and viewid %s took "
"%s seconds" % (itemType, viewId, elapsedtotal), 1)
viewId,
lastViewedAt=lastViewedAt,
updatedAt=updatedAt,
headerOptions=headerOptions)
if plexItems:
itemMth = getattr(itemtypes, itemType)
with itemMth() as method:
method.updateUserdata(plexItems)
def musicvideos(self, embycursor, kodicursor, pdialog):
# Get musicvideos from emby

View File

@ -15,6 +15,7 @@ import playutils as putils
import playlist
import read_embyserver as embyserver
import utils
import embydb_functions
import PlexAPI
@ -23,12 +24,10 @@ import PlexAPI
@utils.logging
class PlaybackUtils():
def __init__(self, item):
self.item = item
self.API = PlexAPI.API(self.item)
self.doUtils = downloadutils.DownloadUtils()
@ -40,21 +39,132 @@ class PlaybackUtils():
self.emby = embyserver.Read_EmbyServer()
self.pl = playlist.Playlist()
def play(self, itemid, dbid=None, seektime=None):
def StartPlay(self, resume=None, resumeItem=None):
self.logMsg("StartPlay called with resume=%s, resumeItem=%s"
% (resume, resumeItem), 1)
# Setup Kodi playlist (e.g. make new one or append or even update)
# Why should we have different behaviour if user is on home screen?!?
# self.homeScreen = xbmc.getCondVisibility('Window.IsActive(home)')
self.playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
# Clear playlist since we're always using PMS playQueues
self.playlist.clear()
self.logMsg("Play called with itemid: %s, dbid: %s, seektime: %s."
% (itemid, dbid, seektime), 1)
self.startPos = max(self.playlist.getposition(), 0) # Can return -1
self.sizePlaylist = self.playlist.size()
self.currentPosition = self.startPos
self.logMsg("Playlist start position: %s" % self.startPos, 1)
self.logMsg("Playlist position we're starting with: %s"
% self.currentPosition, 1)
self.logMsg("Playlist size: %s" % self.sizePlaylist, 1)
doUtils = self.doUtils
item = self.item
API = self.API
self.plexResumeItemId = resumeItem
# Where should we ultimately start playback?
self.resumePost = self.startPos
if resume:
if resume == '0':
resume = None
else:
resume = int(resume)
# Run through the passed PMS playlist and construct playlist
for mediaItem in self.item:
self.AddMediaItemToPlaylist(mediaItem)
# Kick off playback
Player = xbmc.Player()
Player.play(self.playlist, startpos=self.resumePost)
if resume:
try:
Player.seekTime(resume)
except:
self.logMsg("Could not use resume: %s. Start from beginning."
% resume, 0)
def AddMediaItemToPlaylist(self, item):
"""
Feed with ONE media item from PMS json response
(on level with e.g. key=/library/metadata/220493 present)
An item may consist of several parts (e.g. movie in 2 pieces/files)
"""
API = PlexAPI.API(item)
playutils = putils.PlayUtils(item)
# e.g. itemid='219155'
itemid = API.getRatingKey()
# Get DB id from Kodi by using plex id, if that works
embyconn = utils.kodiSQL('emby')
embycursor = embyconn.cursor()
emby = embydb_functions.Embydb_Functions(embycursor)
try:
dbid = emby.getItem_byId(itemid)[0]
except TypeError:
# Trailers and the like that are not in the kodi DB
dbid = None
embyconn.close()
# Get playurls per part and process them
for playurl in playutils.getPlayUrl():
# One new listitem per part
listitem = xbmcgui.ListItem()
# For items that are not (yet) synced to Kodi lib, e.g. trailers
if not dbid:
self.logMsg("Add item to playlist without Kodi DB id", 1)
# Add Plex credentials to url because Kodi will have no headers
playurl = API.addPlexCredentialsToUrl(playurl)
listitem.setPath(playurl)
self.setProperties(playurl, listitem)
# Set artwork already done in setProperties
self.playlist.add(
playurl, listitem, index=self.currentPosition)
self.currentPosition += 1
else:
self.logMsg("Add item to playlist with existing Kodi DB id", 1)
self.pl.addtoPlaylist(dbid, API.getType())
self.currentPosition += 1
# For transcoding only, ask for audio/subs pref
if utils.window('emby_%s.playmethod' % playurl) == "Transcode":
playurl = playutils.audioSubsPref(playurl, listitem)
utils.window('emby_%s.playmethod' % playurl, value="Transcode")
playQueueItemID = API.GetPlayQueueItemID()
# Is this the position where we should start playback?
if playQueueItemID == self.plexResumeItemId:
self.logMsg(
"Figure we should start playback at position %s "
"with playQueueItemID %s"
% (self.currentPosition, playQueueItemID), 2)
self.resumePost = self.currentPosition
# We need to keep track of playQueueItemIDs for Plex Companion
utils.window(
'plex_%s.playQueueItemID' % playurl, API.GetPlayQueueItemID())
utils.window(
'plex_%s.playlistPosition' % playurl, self.currentPosition)
# Log the playlist that we end up with
self.pl.verifyPlaylist()
def play(self, item):
API = PlexAPI.API(item)
listitem = xbmcgui.ListItem()
playutils = putils.PlayUtils(item)
# Set child number to the very last one, because that's what we want
# to play ultimately
API.setChildNumber(-1)
playurl = playutils.getPlayUrl(child=-1)
# e.g. itemid='219155'
itemid = API.getRatingKey()
# Get DB id from Kodi by using plex id, if that works
embyconn = utils.kodiSQL('emby')
embycursor = embyconn.cursor()
emby = embydb_functions.Embydb_Functions(embycursor)
try:
dbid = emby.getItem_byId(itemid)[0]
except TypeError:
# Trailers and the like that are not in the kodi DB
dbid = None
embyconn.close()
playurl = playutils.getPlayUrl()
if not playurl:
return xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, listitem)
@ -82,10 +192,9 @@ class PlaybackUtils():
self.logMsg("Playlist size: %s" % sizePlaylist, 1)
############### RESUME POINT ################
if seektime is None:
userdata = API.getUserData()
seektime = userdata['Resume']
userdata = API.getUserData()
seektime = userdata['Resume']
# We need to ensure we add the intro and additional parts only once.
# Otherwise we get a loop.
@ -132,7 +241,7 @@ class PlaybackUtils():
self.pl.insertintoPlaylist(currentPosition, url=introPlayurl)
introsPlaylist = True
currentPosition += 1
self.logMsg("Key: %s" % API.getKey(), 1)
self.logMsg("Key: %s" % API.getRatingKey(), 1)
self.logMsg("Successfally added trailer number %s" % i, 1)
# Set "working point" to the movie (last one in playlist)
API.setChildNumber(-1)
@ -220,7 +329,7 @@ class PlaybackUtils():
# Set all properties necessary for plugin path playback
item = self.item
# itemid = item['Id']
itemid = self.API.getKey()
itemid = self.API.getRatingKey()
# itemtype = item['Type']
itemtype = self.API.getType()
resume, runtime = self.API.getRuntime()
@ -230,8 +339,9 @@ class PlaybackUtils():
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'))
if itemtype == "episode":
utils.window('%s.refreshid' % embyitem,
value=item.get('parentRatingKey'))
else:
utils.window('%s.refreshid' % embyitem, value=itemid)
@ -282,10 +392,6 @@ class PlaybackUtils():
return externalsubs
def setArtwork(self, listItem):
# Set up item and item info
item = self.item
artwork = self.artwork
# allartwork = artwork.getAllArtwork(item, parentInfo=True)
allartwork = self.API.getAllArtwork(parentInfo=True)
# Set artwork for listitem

View File

@ -21,6 +21,7 @@ class PlayUtils():
def __init__(self, item):
self.item = item
self.API = PlexAPI.API(item)
self.clientInfo = clientinfo.ClientInfo()
@ -28,54 +29,44 @@ class PlayUtils():
self.server = utils.window('emby_server%s' % self.userid)
self.machineIdentifier = utils.window('plex_machineIdentifier')
self.API = PlexAPI.API(item)
def getPlayUrl(self):
"""
Returns a list of playurls, one per part in item
"""
playurls = []
# TODO: multiple media parts for e.g. trailers: replace [0] here
partCount = len(self.item['_children'][0]['_children'])
for partNumber in range(partCount):
playurl = None
self.API.setPartNumber(partNumber)
def getPlayUrl(self, child=0, partIndex=None):
item = self.item
# NO, I am not very fond of this construct!
self.API.setChildNumber(child)
if partIndex is not None:
self.API.setPartNumber(partIndex)
playurl = None
if item.get('Type') in ["Recording","TvChannel"] and item.get('MediaSources') and item['MediaSources'][0]['Protocol'] == "Http":
#Is this the right way to play a Live TV or recordings ?
self.logMsg("File protocol is http (livetv).", 1)
playurl = "%s/emby/Videos/%s/live.m3u8?static=true" % (self.server, item['Id'])
utils.window('emby_%s.playmethod' % playurl, value="DirectPlay")
if self.isDirectPlay():
self.logMsg("File is direct playing.", 1)
playurl = self.API.getTranscodeVideoPath('DirectPlay')
playurl = playurl.encode('utf-8')
# Set playmethod property
utils.window('emby_%s.playmethod' % playurl, "DirectPlay")
# if item.get('MediaSources') and 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.isDirectStream():
self.logMsg("File is direct streaming.", 1)
playurl = self.API.getTranscodeVideoPath('DirectStream')
# Set playmethod property
utils.window('emby_%s.playmethod' % playurl, "DirectStream")
if self.isDirectPlay():
self.logMsg("File is direct playing.", 1)
playurl = self.API.getTranscodeVideoPath('DirectPlay')
playurl = playurl.encode('utf-8')
# Set playmethod property
utils.window('emby_%s.playmethod' % playurl, value="DirectPlay")
elif self.isTranscoding():
self.logMsg("File is transcoding.", 1)
quality = {
'maxVideoBitrate': self.getBitrate()
}
playurl = self.API.getTranscodeVideoPath('Transcode',
quality=quality)
# Set playmethod property
utils.window('emby_%s.playmethod' % playurl, value="Transcode")
elif self.isDirectStream():
self.logMsg("File is direct streaming.", 1)
playurl = self.API.getTranscodeVideoPath('DirectStream')
# Set playmethod property
utils.window('emby_%s.playmethod' % playurl, value="DirectStream")
playurls.append(playurl)
elif self.isTranscoding():
self.logMsg("File is transcoding.", 1)
quality = {
'maxVideoBitrate': self.getBitrate()
}
playurl = self.API.getTranscodeVideoPath(
'Transcode',
quality=quality
)
# Set playmethod property
utils.window('emby_%s.playmethod' % playurl, value="Transcode")
self.logMsg("The playurl is: %s" % playurl, 1)
return playurl
self.logMsg("The playurls are: %s" % playurls, 1)
return playurls
def httpPlay(self):
# Audio, Video, Photo
@ -155,25 +146,24 @@ class PlayUtils():
videoCodec = self.API.getVideoCodec()
codec = videoCodec['videocodec']
resolution = videoCodec['resolution']
if ((utils.settings('transcodeH265') == "true") and
("hevc" in codec) and
(resolution == "1080")):
# Avoid HEVC(H265) 1080p
self.logMsg("Option to transcode 1080P/HEVC enabled.", 0)
# 720p
if ((utils.settings('transcode720H265') == "true") and
("h265" in codec) and
(resolution in "720 1080")):
self.logMsg("Option to transcode 720P/h265 enabled.", 0)
return False
else:
return True
# 1080p
if ((utils.settings('transcodeH265') == "true") and
("h265" in codec) and
(resolution == "1080")):
self.logMsg("Option to transcode 1080P/h265 enabled.", 0)
return False
return True
def isDirectStream(self):
if not self.h265enabled():
return False
elif (utils.settings('transcode720H265') == "true" and
item['MediaSources'][0]['Name'].startswith(("720P/HEVC","720P/H265"))):
# Avoid H265 720p
self.logMsg("Option to transcode 720P/H265 enabled.", 1)
return False
# Requirement: BitRate, supported encoding
# canDirectStream = item['MediaSources'][0]['SupportsDirectStream']
# Plex: always able?!?
@ -192,7 +182,7 @@ class PlayUtils():
server = self.server
itemid = self.API.getKey()
itemid = self.API.getRatingKey()
type = self.API.getType()
# if 'Path' in item and item['Path'].endswith('.strm'):
@ -211,7 +201,7 @@ class PlayUtils():
settings = self.getBitrate()
sourceBitrate = self.API.getBitrate()
sourceBitrate = int(self.API.getDataFromPartOrMedia())
self.logMsg("The add-on settings bitrate is: %s, the video bitrate required is: %s" % (settings, sourceBitrate), 1)
if settings < sourceBitrate:
return False

View File

@ -71,11 +71,9 @@ def jsonrpc(action, arguments = {}):
"id" : 1 ,
"method" : "JSONRPC.Ping" })
elif action.lower() == "playmedia":
fullurl=arguments[0]
resume=arguments[1]
xbmc.Player().play("plugin://plugin.video.plexkodiconnect/"
"?mode=companion&resume=%s&id=%s"
% (resume, fullurl))
"?mode=companion&arguments=%s"
% arguments)
return True
elif arguments:
request=json.dumps({ "id" : 1,

View File

@ -111,16 +111,33 @@ class MyHandler(BaseHTTPRequestHandler):
printDebug("adjusting the volume to %s%%" % volume)
jsonrpc("Application.SetVolume", {"volume": volume})
elif "/playMedia" in request_path:
playQueueVersion = int(params.get('playQueueVersion', 1))
if playQueueVersion < subMgr.playQueueVersion:
# playQueue was updated; ignore this command for now
return
if playQueueVersion > subMgr.playQueueVersion:
# TODO: we should probably update something else now :-)
subMgr.playQueueVersion = playQueueVersion
s.response(getOKMsg(), getPlexHeaders())
resume = params.get('viewOffset', params.get('offset', "0"))
offset = params.get('viewOffset', params.get('offset', "0"))
protocol = params.get('protocol', "http")
address = params.get('address', s.client_address[0])
server = getServerByHost(address)
port = params.get('port', server.get('port', '32400'))
fullurl = protocol+"://"+address+":"+port+params['key']
printDebug("playMedia command -> fullurl: %s" % fullurl)
jsonrpc("playmedia", [fullurl, resume])
try:
containerKey = urlparse(params.get('containerKey')).path
except:
containerKey = ''
regex = re.compile(r'''/playQueues/(\d+)$''')
try:
playQueueID = regex.findall(containerKey)[0]
except IndexError:
playQueueID = ''
jsonrpc("playmedia", params)
subMgr.lastkey = params['key']
subMgr.containerKey = containerKey
subMgr.playQueueID = playQueueID
subMgr.server = server.get('server', 'localhost')
subMgr.port = port
subMgr.protocol = protocol

View File

@ -62,7 +62,7 @@ class plexgdm:
print "PlexGDM: %s" % message
def clientDetails(self, c_id, c_name, c_post, c_product, c_version):
self.client_data = "Content-Type: plex/media-player\r\nResource-Identifier: %s\r\nName: %s\r\nPort: %s\r\nProduct: %s\r\nVersion: %s\r\nProtocol: plex\r\nProtocol-Version: 1\r\nProtocol-Capabilities: navigation,playback,timeline\r\nDevice-Class: HTPC" % ( c_id, c_name, c_post, c_product, c_version )
self.client_data = "Content-Type: plex/media-player\r\nResource-Identifier: %s\r\nName: %s\r\nPort: %s\r\nProduct: %s\r\nVersion: %s\r\nProtocol: plex\r\nProtocol-Version: 1\r\nProtocol-Capabilities: timeline,playback,navigation,mirror,playqueues\r\nDevice-Class: HTPC" % ( c_id, c_name, c_post, c_product, c_version )
self.client_id = c_id
def getClientDetails(self):

View File

@ -12,6 +12,9 @@ class SubscriptionManager:
self.subscribers = {}
self.info = {}
self.lastkey = ""
self.containerKey = ""
self.playQueueID = ''
self.playQueueVersion = 1
self.lastratingkey = ""
self.volume = 0
self.guid = ""
@ -75,12 +78,15 @@ class SubscriptionManager:
if keyid:
self.lastkey = "/library/metadata/%s"%keyid
self.lastratingkey = keyid
ret += ' containerKey="%s"' % (self.lastkey)
ret += ' containerKey="%s"' % (self.containerKey)
ret += ' key="%s"' % (self.lastkey)
ret += ' ratingKey="%s"' % (self.lastratingkey)
if pbmc_server:
(self.server, self.port) = pbmc_server.split(':')
serv = getServerByHost(self.server)
if self.playQueueID:
ret += ' playQueueID="%s"' % self.playQueueID
ret += ' playQueueVersion="%s"' % self.playQueueVersion
ret += ' duration="%s"' % info['duration']
ret += ' seekRange="0-%s"' % info['duration']
ret += ' controllable="%s"' % self.controllable()
@ -119,12 +125,15 @@ class SubscriptionManager:
for p in players.values():
info = self.playerprops[p.get('playerid')]
params = {}
params['containerKey'] = (self.lastkey or "/library/metadata/900000")
params['containerKey'] = (self.containerKey or "/library/metadata/900000")
if self.playQueueID:
params['playQueueID'] = self.playQueueID
params['key'] = (self.lastkey or "/library/metadata/900000")
params['ratingKey'] = (self.lastratingkey or "900000")
params['state'] = info['state']
params['time'] = info['time']
params['duration'] = info['duration']
params['playQueueVersion'] = self.playQueueVersion
serv = getServerByHost(self.server)
url = serv.get('protocol', 'http') + '://' \
+ serv.get('server', 'localhost') + ':' \
@ -134,11 +143,9 @@ class SubscriptionManager:
printDebug("params: %s" % params)
printDebug("players: %s" % players)
printDebug("sent server notification with state = %s" % params['state'])
WINDOW = xbmcgui.Window(10000)
WINDOW.setProperty('plexbmc.nowplaying.sent', '1')
def controllable(self):
return "playPause,play,stop,skipPrevious,skipNext,volume,stepBack,stepForward,seekTo"
return "volume,shuffle,repeat,audioStream,videoStream,subtitleStream,skipPrevious,skipNext,seekTo,stepBack,stepForward,stop,playPause"
def addSubscriber(self, protocol, host, port, uuid, commandID):
sub = Subscriber(protocol, host, port, uuid, commandID)
@ -211,7 +218,13 @@ class Subscriber:
+ "/:/timeline"
# Override some headers
headerOptions = {
'Accept': '*/*'
'Content-Range': 'bytes 0-/-1',
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.52 Safari/537.17',
'Accept': '*/*',
'X-Plex-Username': 'croneter',
'Connection': 'keep-alive',
'X-Plex-Client-Capabilities': 'protocols=shoutcast,http-video;videoDecoders=h264{profile:high&resolution:1080&level:51};audioDecoders=mp3,aac,dts{bitrate:800000&channels:8},ac3{bitrate:800000&channels:8}',
'X-Plex-Client-Profile-Extra': 'add-transcode-target-audio-codec(type=videoProfile&context=streaming&protocol=*&audioCodec=dca,ac3)'
}
response = self.download.downloadUrl(
url,
@ -220,6 +233,6 @@ class Subscriber:
headerOptions=headerOptions)
# if not requests.post(self.host, self.port, "/:/timeline", msg, getPlexHeaders(), self.protocol):
# subMgr.removeSubscriber(self.uuid)
if response in [False, 401]:
if response in [False, None, 401]:
subMgr.removeSubscriber(self.uuid)
subMgr = SubscriptionManager()

View File

@ -13,7 +13,6 @@ import utils
import downloadutils
import PlexAPI
import librarysync
##################################################################################################

View File

@ -9,7 +9,7 @@ import sqlite3
import time
import unicodedata
import xml.etree.ElementTree as etree
from functools import wraps, update_wrapper
from functools import wraps
from datetime import datetime, timedelta
from calendar import timegm
@ -24,19 +24,35 @@ import xbmcvfs
addonName = xbmcaddon.Addon().getAddonInfo('name')
def ThreadMethodsStopsync(cls):
def LogTime(func):
"""
Decorator to replace stopThread method to include the Kodi window property
'emby_shouldStop'
Decorator for functions and methods to log the time it took to run the code
"""
@wraps(func)
def wrapper(*args, **kwargs):
starttotal = datetime.now()
result = func(*args, **kwargs)
elapsedtotal = datetime.now() - starttotal
logMsg('%s %s' % (addonName, func.__name__),
'It took %s to run the function.' % (elapsedtotal), 1)
return result
return wrapper
Use with any library sync threads. @ThreadMethods still required FIRST
def ThreadMethodsAdditionalStop(windowAttribute):
"""
def threadStopped(self):
return (self._threadStopped or
self._abortMonitor.abortRequested() or
window('emby_shouldStop') == "true")
cls.threadStopped = threadStopped
return cls
Decorator to replace stopThread method to include the Kodi windowAttribute
Use with any sync threads. @ThreadMethods still required FIRST
"""
def wrapper(cls):
def threadStopped(self):
return (self._threadStopped or
self._abortMonitor.abortRequested() or
window(windowAttribute) == "true")
cls.threadStopped = threadStopped
return cls
return wrapper
def ThreadMethodsAdditionalSuspend(windowAttribute):
@ -48,8 +64,8 @@ def ThreadMethodsAdditionalSuspend(windowAttribute):
"""
def wrapper(cls):
def threadSuspended(self):
return (self._threadSuspended or True if
window(windowAttribute) == 'true' else False)
return (self._threadSuspended or
window(windowAttribute) == 'true')
cls.threadSuspended = threadSuspended
return cls
return wrapper
@ -140,7 +156,6 @@ def getUnixTimestamp(secondsIntoTheFuture=None):
def logMsg(title, msg, level=1):
# Get the logLevel set in UserClient
try:
logLevel = int(window('emby_logLevel'))
@ -155,13 +170,19 @@ def logMsg(title, msg, level=1):
xbmc.log("%s -> %s : %s" % (
title, func.co_name, msg))
except UnicodeEncodeError:
xbmc.log("%s -> %s : %s" % (
title, func.co_name, msg.encode('utf-8')))
try:
xbmc.log("%s -> %s : %s" % (
title, func.co_name, msg.encode('utf-8')))
except:
xbmc.log("%s -> %s : %s" % (title, func.co_name, 'COULDNT LOG'))
else:
try:
xbmc.log("%s -> %s" % (title, msg))
except UnicodeEncodeError:
xbmc.log("%s -> %s" % (title, msg.encode('utf-8')))
try:
xbmc.log("%s -> %s" % (title, msg.encode('utf-8')))
except:
xbmc.log("%s -> %s " % (title, 'COULDNT LOG'))
def window(property, value=None, clear=False, windowid=10000):

View File

@ -28,6 +28,7 @@
<setting id="myplexlogin" label="Log into plex.tv?" type="bool" default="true" />
<setting id="plexLogin" label="plex.tv username" type="text" default="" visible="eq(-1,true)" />
<setting id="plexhome" label="Plex home in use" type="bool" default="true" visible="false" />
<setting id="plexToken" label="plexToken" type="text" default="" visible="false" />
</category>
<category label="Sync Options">