Merge branch 'develop'

This commit is contained in:
tomkat83 2017-01-09 21:29:45 +01:00
commit 2836f707c5
37 changed files with 2979 additions and 2077 deletions

View file

@ -1,13 +1,11 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="plugin.video.plexkodiconnect" <addon id="plugin.video.plexkodiconnect"
name="PlexKodiConnect" name="PlexKodiConnect"
version="1.5.1" version="1.5.2"
provider-name="croneter"> provider-name="croneter">
<requires> <requires>
<import addon="xbmc.python" version="2.1.0"/> <import addon="xbmc.python" version="2.1.0"/>
<import addon="script.module.requests" version="2.3.0" /> <import addon="script.module.requests" version="2.3.0" />
<import addon="plugin.video.plexkodiconnect.movies" version="0.01" />
<import addon="plugin.video.plexkodiconnect.tvshows" version="0.01" />
</requires> </requires>
<extension point="xbmc.python.pluginsource" <extension point="xbmc.python.pluginsource"
library="default.py"> library="default.py">

View file

@ -1,3 +1,14 @@
version 1.5.2 (beta only)
A DATABASE RESET IS ABSOLUTELY NECESSARY
- Plex Companion is completely rewired and should now handly anything you throw at it
- New playback startup mechanism for plugin paths
- Krypton: add ratings and IMDB id for movies
- Krypton: add ratings and theTvDB id for TV shows
- Don't support Plex Companion mirror
- Fix for Plex Companion not showing up
- Code rebranding from Emby to Plex, including a plex.db database :-)
- Lots of code refactoring and code optimizations
version 1.5.1 (beta only) version 1.5.1 (beta only)
- Fix playstate and PMS item changes not working/not propagating anymore (caused by a change Plex made with the websocket interface). UPGRADE YOUR PMS!! - Fix playstate and PMS item changes not working/not propagating anymore (caused by a change Plex made with the websocket interface). UPGRADE YOUR PMS!!
- Improvements to the way PKC behaves if the PMS goes offline - Improvements to the way PKC behaves if the PMS goes offline

View file

@ -3,36 +3,38 @@
############################################################################### ###############################################################################
import logging import logging
import os from os import path as os_path
import sys from sys import path as sys_path, argv
import urlparse from urlparse import parse_qsl
import xbmc from xbmc import translatePath, sleep, executebuiltin
import xbmcaddon from xbmcaddon import Addon
import xbmcgui from xbmcgui import ListItem, Dialog
from xbmcplugin import setResolvedUrl
_addon = Addon(id='plugin.video.plexkodiconnect')
_addon = xbmcaddon.Addon(id='plugin.video.plexkodiconnect')
try: try:
_addon_path = _addon.getAddonInfo('path').decode('utf-8') _addon_path = _addon.getAddonInfo('path').decode('utf-8')
except TypeError: except TypeError:
_addon_path = _addon.getAddonInfo('path').decode() _addon_path = _addon.getAddonInfo('path').decode()
try: try:
_base_resource = xbmc.translatePath(os.path.join( _base_resource = translatePath(os_path.join(
_addon_path, _addon_path,
'resources', 'resources',
'lib')).decode('utf-8') 'lib')).decode('utf-8')
except TypeError: except TypeError:
_base_resource = xbmc.translatePath(os.path.join( _base_resource = translatePath(os_path.join(
_addon_path, _addon_path,
'resources', 'resources',
'lib')).decode() 'lib')).decode()
sys.path.append(_base_resource) sys_path.append(_base_resource)
############################################################################### ###############################################################################
import entrypoint import entrypoint
import utils from utils import window, pickl_window, reset, passwordsXML
from pickler import unpickle_me
from PKC_listitem import convert_PKC_to_listitem
############################################################################### ###############################################################################
@ -43,34 +45,47 @@ log = logging.getLogger("PLEX.default")
############################################################################### ###############################################################################
ARGV = argv
HANDLE = int(argv[1])
class Main(): class Main():
# MAIN ENTRY POINT # MAIN ENTRY POINT
#@utils.profiling() # @utils.profiling()
def __init__(self): def __init__(self):
log.debug("Full sys.argv received: %s" % ARGV)
# Parse parameters # Parse parameters
log.warn("Full sys.argv received: %s" % sys.argv) params = dict(parse_qsl(ARGV[2][1:]))
base_url = sys.argv[0]
params = urlparse.parse_qs(sys.argv[2][1:])
try: try:
mode = params['mode'][0] mode = params['mode']
itemid = params.get('id', '') itemid = params.get('id', '')
if itemid:
try:
itemid = itemid[0]
except:
pass
except: except:
params = {}
mode = "" mode = ""
itemid = ''
if mode == 'play':
# Put the request into the "queue"
while window('plex_play_new_item'):
sleep(20)
window('plex_play_new_item',
value='%s%s' % (mode, ARGV[2]))
# Wait for the result
while not pickl_window('plex_result'):
sleep(20)
result = unpickle_me()
if result is None:
log.error('Error encountered, aborting')
setResolvedUrl(HANDLE, False, ListItem())
elif result.listitem:
listitem = convert_PKC_to_listitem(result.listitem)
setResolvedUrl(HANDLE, True, listitem)
return
modes = { modes = {
'reset': reset,
'reset': utils.reset,
'resetauth': entrypoint.resetAuth, 'resetauth': entrypoint.resetAuth,
'play': entrypoint.doPlayback, 'passwords': passwordsXML,
'passwords': utils.passwordsXML,
'channels': entrypoint.BrowseChannels, 'channels': entrypoint.BrowseChannels,
'channelsfolder': entrypoint.BrowseChannels, 'channelsfolder': entrypoint.BrowseChannels,
'browsecontent': entrypoint.BrowseContent, 'browsecontent': entrypoint.BrowseContent,
@ -79,7 +94,6 @@ class Main():
'inprogressepisodes': entrypoint.getInProgressEpisodes, 'inprogressepisodes': entrypoint.getInProgressEpisodes,
'recentepisodes': entrypoint.getRecentEpisodes, 'recentepisodes': entrypoint.getRecentEpisodes,
'refreshplaylist': entrypoint.refreshPlaylist, 'refreshplaylist': entrypoint.refreshPlaylist,
'companion': entrypoint.plexCompanion,
'switchuser': entrypoint.switchPlexUser, 'switchuser': entrypoint.switchPlexUser,
'deviceid': entrypoint.resetDeviceId, 'deviceid': entrypoint.resetDeviceId,
'delete': entrypoint.deleteItem, 'delete': entrypoint.deleteItem,
@ -89,24 +103,24 @@ class Main():
'watchlater': entrypoint.watchlater, 'watchlater': entrypoint.watchlater,
'enterPMS': entrypoint.enterPMS, 'enterPMS': entrypoint.enterPMS,
'togglePlexTV': entrypoint.togglePlexTV, 'togglePlexTV': entrypoint.togglePlexTV,
'playwatchlater': entrypoint.playWatchLater 'Plex_Node': entrypoint.Plex_Node
} }
if "/extrafanart" in sys.argv[0]: if "/extrafanart" in ARGV[0]:
plexpath = sys.argv[2][1:] plexpath = ARGV[2][1:]
plexid = params.get('id', [""])[0] plexid = params.get('id', [""])
entrypoint.getExtraFanArt(plexid, plexpath) entrypoint.getExtraFanArt(plexid, plexpath)
entrypoint.getVideoFiles(plexid, plexpath) entrypoint.getVideoFiles(plexid, plexpath)
return return
if mode == 'fanart': if mode == 'fanart':
log.info('User requested fanarttv refresh') log.info('User requested fanarttv refresh')
utils.window('plex_runLibScan', value='fanart') window('plex_runLibScan', value='fanart')
# Called by e.g. 3rd party plugin video extras # Called by e.g. 3rd party plugin video extras
if ("/Extras" in sys.argv[0] or "/VideoFiles" in sys.argv[0] or if ("/Extras" in ARGV[0] or "/VideoFiles" in ARGV[0] or
"/Extras" in sys.argv[2]): "/Extras" in ARGV[2]):
plexId = params.get('id', [None])[0] plexId = params.get('id', None)
entrypoint.getVideoFiles(plexId, params) entrypoint.getVideoFiles(plexId, params)
if modes.get(mode): if modes.get(mode):
@ -117,59 +131,60 @@ class Main():
modes[mode](itemid, dbid) modes[mode](itemid, dbid)
elif mode in ("nextup", "inprogressepisodes"): elif mode in ("nextup", "inprogressepisodes"):
limit = int(params['limit'][0]) limit = int(params['limit'])
modes[mode](itemid, limit) modes[mode](itemid, limit)
elif mode in ("channels","getsubfolders"): elif mode in ("channels","getsubfolders"):
modes[mode](itemid) modes[mode](itemid)
elif mode == "browsecontent": elif mode == "browsecontent":
modes[mode](itemid, params.get('type',[""])[0], params.get('folderid',[""])[0]) modes[mode](itemid, params.get('type',[""]), params.get('folderid',[""]))
elif mode == 'browseplex': elif mode == 'browseplex':
modes[mode]( modes[mode](
itemid, itemid,
params.get('type', [""])[0], params.get('type', [""]),
params.get('folderid', [""])[0]) params.get('folderid', [""]))
elif mode in ('ondeck', 'recentepisodes'): elif mode in ('ondeck', 'recentepisodes'):
modes[mode]( modes[mode](
itemid, itemid,
params.get('type', [""])[0], params.get('type', [""]),
params.get('tagname', [""])[0], params.get('tagname', [""]),
int(params.get('limit', [""])[0])) int(params.get('limit', [""])))
elif mode == "channelsfolder": elif mode == "channelsfolder":
folderid = params['folderid'][0] folderid = params['folderid']
modes[mode](itemid, folderid) modes[mode](itemid, folderid)
elif mode == "companion": elif mode == "companion":
modes[mode](itemid, params=sys.argv[2]) modes[mode](itemid, params=ARGV[2])
elif mode == 'playwatchlater': elif mode == 'Plex_Node':
modes[mode](params.get('id')[0], params.get('viewOffset')[0]) modes[mode](params.get('id'),
params.get('viewOffset'),
params.get('plex_type'))
else: else:
modes[mode]() modes[mode]()
else: else:
# Other functions # Other functions
if mode == "settings": if mode == "settings":
xbmc.executebuiltin('Addon.OpenSettings(plugin.video.plexkodiconnect)') executebuiltin('Addon.OpenSettings(plugin.video.plexkodiconnect)')
elif mode in ("manualsync", "repair"): elif mode in ("manualsync", "repair"):
if utils.window('plex_online') != "true": if window('plex_online') != "true":
# Server is not online, do not run the sync # Server is not online, do not run the sync
xbmcgui.Dialog().ok( Dialog().ok(
"PlexKodiConnect", "PlexKodiConnect",
"Unable to run the sync, the add-on is not connected " "Unable to run the sync, the add-on is not connected "
"to a Plex server.") "to a Plex server.")
log.error("Not connected to a PMS.") log.error("Not connected to a PMS.")
else: else:
if mode == 'repair': if mode == 'repair':
utils.window('plex_runLibScan', value="repair") window('plex_runLibScan', value="repair")
log.info("Requesting repair lib sync") log.info("Requesting repair lib sync")
elif mode == 'manualsync': elif mode == 'manualsync':
log.info("Requesting full library scan") log.info("Requesting full library scan")
utils.window('plex_runLibScan', value="full") window('plex_runLibScan', value="full")
elif mode == "texturecache": elif mode == "texturecache":
utils.window('plex_runLibScan', value='del_textures') window('plex_runLibScan', value='del_textures')
else: else:
entrypoint.doMainListing() entrypoint.doMainListing()

View file

@ -0,0 +1,334 @@
# -*- coding: utf-8 -*-
###############################################################################
import logging
from xbmcgui import ListItem
###############################################################################
log = logging.getLogger("PLEX."+__name__)
###############################################################################
def convert_PKC_to_listitem(PKC_listitem):
"""
Insert a PKC_listitem and you will receive a valid XBMC listitem
"""
listitem = ListItem()
for func, args in PKC_listitem.data.items():
if isinstance(args, list):
for arg in args:
getattr(listitem, func)(*arg)
elif isinstance(args, dict):
for arg in args.items():
getattr(listitem, func)(*arg)
elif args is None:
continue
else:
getattr(listitem, func)(args)
return listitem
class PKC_ListItem(object):
"""
Imitates xbmcgui.ListItem and its functions. Pass along PKC_Listitem().data
when pickling!
WARNING: set/get path only via setPath and getPath! (not getProperty)
"""
def __init__(self, label=None, label2=None, path=None):
self.data = {
'addStreamInfo': [], # (type, values: dict { label: value })
'setArt': [], # dict: { label: value }
'setInfo': {}, # type: infoLabel (dict { label: value })
'setLabel': label, # string
'setLabel2': label2, # string
'setPath': path, # string
'setProperty': {}, # (key, value)
'setSubtitles': [], # string
}
def addContextMenuItems(self, items, replaceItems):
"""
Adds item(s) to the context menu for media lists.
items : list - [(label, action,)*] A list of tuples consisting of label
and action pairs.
- label : string or unicode - item's label.
- action : string or unicode - any built-in function to perform.
replaceItes : [opt] bool - True=only your items will show/False=your
items will be amdded to context menu(Default).
List of functions - http://kodi.wiki/view/List_of_Built_In_Functions
*Note, You can use the above as keywords for arguments and skip
certain optional arguments.
Once you use a keyword, all following arguments require the keyword.
"""
raise NotImplementedError
def addStreamInfo(self, type, values):
"""
Add a stream with details.
type : string - type of stream(video/audio/subtitle).
values : dictionary - pairs of { label: value }.
- Video Values:
- codec : string (h264)
- aspect : float (1.78)
- width : integer (1280)
- height : integer (720)
- duration : integer (seconds)
- Audio Values:
- codec : string (dts)
- language : string (en)
- channels : integer (2)
- Subtitle Values:
- language : string (en)
"""
self.data['addStreamInfo'].append((type, values))
def getLabel(self):
"""
Returns the listitem label
"""
return self.data['setLabel']
def getLabel2(self):
"""
Returns the listitem label.
"""
return self.data['setLabel2']
def getMusicInfoTag(self):
"""
returns the MusicInfoTag for this item.
"""
raise NotImplementedError
def getProperty(self, key):
"""
Returns a listitem property as a string, similar to an infolabel.
key : string - property name.
*Note, Key is NOT case sensitive.
You can use the above as keywords for arguments and skip certain
optional arguments.
Once you use a keyword, all following arguments require the keyword.
"""
return self.data['setProperty'].get(key)
def getVideoInfoTag(self):
"""
returns the VideoInfoTag for this item
"""
raise NotImplementedError
def getdescription(self):
"""
Returns the description of this PlayListItem
"""
raise NotImplementedError
def getduration(self):
"""
Returns the duration of this PlayListItem
"""
raise NotImplementedError
def getfilename(self):
"""
Returns the filename of this PlayListItem.
"""
raise NotImplementedError
def isSelected(self):
"""
Returns the listitem's selected status
"""
raise NotImplementedError
def select(self):
"""
Sets the listitem's selected status.
selected : bool - True=selected/False=not selected
"""
raise NotImplementedError
def setArt(self, values):
"""
Sets the listitem's art
values : dictionary - pairs of { label: value }.
Some default art values (any string possible):
- thumb : string - image filename
- poster : string - image filename
- banner : string - image filename
- fanart : string - image filename
- clearart : string - image filename
- clearlogo : string - image filename
- landscape : string - image filename
- icon : string - image filename
"""
self.data['setArt'].append(values)
def setContentLookup(self, enable):
"""
Enable or disable content lookup for item.
If disabled, HEAD requests to e.g determine mime type will not be sent.
enable : bool
"""
raise NotImplementedError
def setInfo(self, type, infoLabels):
"""
type : string - type of media(video/music/pictures).
infoLabels : dictionary - pairs of { label: value }. *Note, To set
pictures exif info, prepend 'exif:' to the label. Exif values must be
passed as strings, separate value pairs with a comma. (eg.
{'exif:resolution': '720,480'}
See CPictureInfoTag::TranslateString in PictureInfoTag.cpp for valid
strings. You can use the above as keywords for arguments and skip
certain optional arguments.
Once you use a keyword, all following arguments require the keyword.
- General Values that apply to all types:
- count : integer (12) - can be used to store an id for later, or
for sorting purposes
- size : long (1024) - size in bytes
- date : string (d.m.Y / 01.01.2009) - file date
- Video Values:
- genre : string (Comedy)
- year : integer (2009)
- episode : integer (4)
- season : integer (1)
- top250 : integer (192)
- tracknumber : integer (3)
- rating : float (6.4) - range is 0..10
- userrating : integer (9) - range is 1..10
- watched : depreciated - use playcount instead
- playcount : integer (2) - number of times this item has been
played
- overlay : integer (2) - range is 0..8. See GUIListItem.h for
values
- cast : list (["Michal C. Hall","Jennifer Carpenter"]) - if
provided a list of tuples cast will be interpreted as castandrole
- castandrole : list of tuples ([("Michael C.
Hall","Dexter"),("Jennifer Carpenter","Debra")])
- director : string (Dagur Kari)
- mpaa : string (PG-13)
- plot : string (Long Description)
- plotoutline : string (Short Description)
- title : string (Big Fan)
- originaltitle : string (Big Fan)
- sorttitle : string (Big Fan)
- duration : integer (245) - duration in seconds
- studio : string (Warner Bros.)
- tagline : string (An awesome movie) - short description of movie
- writer : string (Robert D. Siegel)
- tvshowtitle : string (Heroes)
- premiered : string (2005-03-04)
- status : string (Continuing) - status of a TVshow
- code : string (tt0110293) - IMDb code
- aired : string (2008-12-07)
- credits : string (Andy Kaufman) - writing credits
- lastplayed : string (Y-m-d h:m:s = 2009-04-05 23:16:04)
- album : string (The Joshua Tree)
- artist : list (['U2'])
- votes : string (12345 votes)
- trailer : string (/home/user/trailer.avi)
- dateadded : string (Y-m-d h:m:s = 2009-04-05 23:16:04)
- mediatype : string - "video", "movie", "tvshow", "season",
"episode" or "musicvideo"
- Music Values:
- tracknumber : integer (8)
- discnumber : integer (2)
- duration : integer (245) - duration in seconds
- year : integer (1998)
- genre : string (Rock)
- album : string (Pulse)
- artist : string (Muse)
- title : string (American Pie)
- rating : string (3) - single character between 0 and 5
- lyrics : string (On a dark desert highway...)
- playcount : integer (2) - number of times this item has been
played
- lastplayed : string (Y-m-d h:m:s = 2009-04-05 23:16:04)
- Picture Values:
- title : string (In the last summer-1)
- picturepath : string (/home/username/pictures/img001.jpg)
- exif : string (See CPictureInfoTag::TranslateString in
PictureInfoTag.cpp for valid strings)
"""
self.data['setInfo'][type] = infoLabels
def setLabel(self, label):
"""
Sets the listitem's label.
label : string or unicode - text string.
"""
self.data['setLabel'] = label
def setLabel2(self, label):
"""
Sets the listitem's label2.
label : string or unicode - text string.
"""
self.data['setLabel2'] = label
def setMimeType(self, mimetype):
"""
Sets the listitem's mimetype if known.
mimetype : string or unicode - mimetype.
If known prehand, this can (but does not have to) avoid HEAD requests
being sent to HTTP servers to figure out file type.
"""
raise NotImplementedError
def setPath(self, path):
"""
Sets the listitem's path.
path : string or unicode - path, activated when item is clicked.
*Note, You can use the above as keywords for arguments.
"""
self.data['setPath'] = path
def setProperty(self, key, value):
"""
Sets a listitem property, similar to an infolabel.
key : string - property name.
value : string or unicode - value of property.
*Note, Key is NOT case sensitive.
You can use the above as keywords for arguments and skip certain
optional arguments. Once you use a keyword, all following arguments
require the keyword.
Some of these are treated internally by XBMC, such as the
'StartOffset' property, which is the offset in seconds at which to
start playback of an item. Others may be used in the skin to add extra
information, such as 'WatchedCount' for tvshow items
"""
self.data['setProperty'][key] = value
def setSubtitles(self, subtitles):
"""
Sets subtitles for this listitem. Pass in a list of filepaths
example:
- listitem.setSubtitles(['special://temp/example.srt',
'http://example.com/example.srt' ])
"""
self.data['setSubtitles'].extend(([subtitles],))

View file

@ -50,8 +50,9 @@ import downloadutils
from utils import window, settings, language as lang, tryDecode, tryEncode, \ from utils import window, settings, language as lang, tryDecode, tryEncode, \
DateToKodi, KODILANGUAGE DateToKodi, KODILANGUAGE
from PlexFunctions import PLEX_TO_KODI_TIMEFACTOR, PMSHttpsEnabled, \ from PlexFunctions import PLEX_TO_KODI_TIMEFACTOR, PMSHttpsEnabled, \
REMAP_TYPE_FROM_PLEXTYPE REMAP_TYPE_FROM_PLEXTYPE, PLEX_TYPE_MOVIE, PLEX_TYPE_SHOW, \
import embydb_functions as embydb PLEX_TYPE_EPISODE
import plexdb_functions as plexdb
############################################################################### ###############################################################################
@ -1646,7 +1647,7 @@ class API():
If not found, empty str is returned If not found, empty str is returned
""" """
return self.item.attrib.get('playQueueItemID', '') return self.item.attrib.get('playQueueItemID')
def getDataFromPartOrMedia(self, key): def getDataFromPartOrMedia(self, key):
""" """
@ -1915,9 +1916,9 @@ class API():
# Return the saved Plex id's, if applicable # Return the saved Plex id's, if applicable
# Always seek collection's ids since not provided by PMS # Always seek collection's ids since not provided by PMS
if collection is False: if collection is False:
if media_type == 'movie': if media_type == PLEX_TYPE_MOVIE:
mediaId = self.getProvider('imdb') mediaId = self.getProvider('imdb')
elif media_type == 'show': elif media_type == PLEX_TYPE_SHOW:
mediaId = self.getProvider('tvdb') mediaId = self.getProvider('tvdb')
if mediaId is not None: if mediaId is not None:
return mediaId return mediaId
@ -1927,7 +1928,7 @@ class API():
log.info('Start movie set/collection lookup on themoviedb') log.info('Start movie set/collection lookup on themoviedb')
apiKey = settings('themoviedbAPIKey') apiKey = settings('themoviedbAPIKey')
if media_type == 'show': if media_type == PLEX_TYPE_SHOW:
media_type = 'tv' media_type = 'tv'
title = item.get('title', '') title = item.get('title', '')
# if the title has the year in remove it as tmdb cannot deal with it... # if the title has the year in remove it as tmdb cannot deal with it...
@ -2305,10 +2306,10 @@ class API():
kodiindex = 0 kodiindex = 0
for stream in mediastreams: for stream in mediastreams:
index = stream.attrib['id'] index = stream.attrib['id']
# Since Emby returns all possible tracks together, have to pull # Since plex returns all possible tracks together, have to pull
# only external subtitles. # only external subtitles.
key = stream.attrib.get('key') key = stream.attrib.get('key')
# IsTextSubtitleStream if true, is available to download from emby. # IsTextSubtitleStream if true, is available to download from plex.
if stream.attrib.get('streamType') == "3" and key: if stream.attrib.get('streamType') == "3" and key:
# Direct stream # Direct stream
url = ("%s%s" % (self.server, key)) url = ("%s%s" % (self.server, key))
@ -2318,7 +2319,7 @@ class API():
externalsubs.append(url) externalsubs.append(url)
kodiindex += 1 kodiindex += 1
mapping = json.dumps(mapping) mapping = json.dumps(mapping)
window('emby_%s.indexMapping' % playurl, value=mapping) window('plex_%s.indexMapping' % playurl, value=mapping)
log.info('Found external subs: %s' % externalsubs) log.info('Found external subs: %s' % externalsubs)
return externalsubs return externalsubs
@ -2393,7 +2394,7 @@ class API():
# listItem.setProperty('isPlayable', 'true') # listItem.setProperty('isPlayable', 'true')
# listItem.setProperty('isFolder', 'true') # listItem.setProperty('isFolder', 'true')
# Further stuff # Further stuff
listItem.setIconImage('DefaultPicture.png') listItem.setArt({'icon': 'DefaultPicture.png'})
return listItem return listItem
def _createVideoListItem(self, def _createVideoListItem(self,
@ -2456,21 +2457,21 @@ class API():
"s%.2de%.2d" % (season, episode)) "s%.2de%.2d" % (season, episode))
if appendSxxExx is True: if appendSxxExx is True:
title = "S%.2dE%.2d - %s" % (season, episode, title) title = "S%.2dE%.2d - %s" % (season, episode, title)
listItem.setIconImage('DefaultTVShows.png') listItem.setArt({'icon': 'DefaultTVShows.png'})
if appendShowTitle is True: if appendShowTitle is True:
title = "%s - %s " % (show, title) title = "%s - %s " % (show, title)
elif typus == "movie": elif typus == "movie":
listItem.setIconImage('DefaultMovies.png') listItem.setArt({'icon': 'DefaultMovies.png'})
else: else:
# E.g. clips, trailers, ... # E.g. clips, trailers, ...
listItem.setIconImage('DefaultVideo.png') listItem.setArt({'icon': 'DefaultVideo.png'})
plexId = self.getRatingKey() plexId = self.getRatingKey()
listItem.setProperty('plexid', plexId) listItem.setProperty('plexid', plexId)
with embydb.GetEmbyDB() as emby_db: with plexdb.Get_Plex_DB() as plex_db:
try: try:
listItem.setProperty('dbid', listItem.setProperty('dbid',
str(emby_db.getItem_byId(plexId)[0])) str(plex_db.getItem_byId(plexId)[0]))
except TypeError: except TypeError:
pass pass
# Expensive operation # Expensive operation
@ -2563,3 +2564,68 @@ class API():
line1=lang(39031) + url, line1=lang(39031) + url,
line2=lang(39032)) line2=lang(39032))
return resp return resp
def set_listitem_artwork(self, listitem):
"""
Set all artwork to the listitem
"""
allartwork = self.getAllArtwork(parentInfo=True)
arttypes = {
'poster': "Primary",
'tvshow.poster': "Thumb",
'clearart': "Art",
'tvshow.clearart': "Art",
'clearart': "Primary",
'tvshow.clearart': "Primary",
'clearlogo': "Logo",
'tvshow.clearlogo': "Logo",
'discart': "Disc",
'fanart_image': "Backdrop",
'landscape': "Backdrop",
"banner": "Banner"
}
for arttype in arttypes:
art = arttypes[arttype]
if art == "Backdrop":
try:
# Backdrop is a list, grab the first backdrop
self._set_listitem_artprop(listitem,
arttype,
allartwork[art][0])
except:
pass
else:
self._set_listitem_artprop(listitem, arttype, allartwork[art])
def _set_listitem_artprop(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 set_playback_win_props(self, playurl, listitem):
"""
Set all properties necessary for plugin path playback for listitem
"""
itemtype = self.getType()
userdata = self.getUserData()
plexitem = "plex_%s" % playurl
window('%s.runtime' % plexitem, value=str(userdata['Runtime']))
window('%s.type' % plexitem, value=itemtype)
window('%s.itemid' % plexitem, value=self.getRatingKey())
window('%s.playcount' % plexitem, value=str(userdata['PlayCount']))
if itemtype == PLEX_TYPE_EPISODE:
window('%s.refreshid' % plexitem, value=self.getParentRatingKey())
else:
window('%s.refreshid' % plexitem, value=self.getRatingKey())
# Append external subtitles to stream
playmethod = window('%s.playmethod' % plexitem)
if playmethod in ("DirectStream", "DirectPlay"):
subtitles = self.externalSubs(playurl)
listitem.setSubtitles(subtitles)

View file

@ -1,19 +1,17 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import logging import logging
import threading from threading import Thread
import traceback
import socket
import Queue import Queue
from socket import SHUT_RDWR
import xbmc from xbmc import sleep
from utils import settings, ThreadMethodsAdditionalSuspend, ThreadMethods from utils import settings, ThreadMethodsAdditionalSuspend, ThreadMethods
from plexbmchelper import listener, plexgdm, subscribers, functions, \ from plexbmchelper import listener, plexgdm, subscribers, functions, \
httppersist, plexsettings httppersist, plexsettings
from PlexFunctions import ParseContainerKey, GetPlayQueue, \ from PlexFunctions import ParseContainerKey, KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE
ConvertPlexToKodiTime
import playlist
import player import player
from entrypoint import Plex_Node
############################################################################### ###############################################################################
@ -24,24 +22,23 @@ log = logging.getLogger("PLEX."+__name__)
@ThreadMethodsAdditionalSuspend('plex_serverStatus') @ThreadMethodsAdditionalSuspend('plex_serverStatus')
@ThreadMethods @ThreadMethods
class PlexCompanion(threading.Thread): class PlexCompanion(Thread):
""" """
Initialize with a Queue for callbacks
""" """
def __init__(self): def __init__(self, callback=None):
log.info("----===## Starting PlexCompanion ##===----") log.info("----===## Starting PlexCompanion ##===----")
if callback is not None:
self.mgr = callback
self.settings = plexsettings.getSettings() self.settings = plexsettings.getSettings()
# Start GDM for server/client discovery # Start GDM for server/client discovery
self.client = plexgdm.plexgdm() self.client = plexgdm.plexgdm()
self.client.clientDetails(self.settings) self.client.clientDetails(self.settings)
log.debug("Registration string is: %s " log.debug("Registration string is: %s "
% self.client.getClientDetails()) % self.client.getClientDetails())
# Initialize playlist/queue stuff
self.playlist = playlist.Playlist('video')
# kodi player instance # kodi player instance
self.player = player.Player() self.player = player.Player()
threading.Thread.__init__(self) Thread.__init__(self)
def _getStartItem(self, string): def _getStartItem(self, string):
""" """
@ -62,62 +59,48 @@ class PlexCompanion(threading.Thread):
def processTasks(self, task): def processTasks(self, task):
""" """
Processes tasks picked up e.g. by Companion listener Processes tasks picked up e.g. by Companion listener, e.g.
{'action': 'playlist',
task = { 'data': {'address': 'xyz.plex.direct',
'action': 'playlist' 'commandID': '7',
'data': as received from Plex companion 'containerKey': '/playQueues/6669?own=1&repeat=0&window=200',
} 'key': '/library/metadata/220493',
'machineIdentifier': 'xyz',
'offset': '0',
'port': '32400',
'protocol': 'https',
'token': 'transient-cd2527d1-0484-48e0-a5f7-f5caa7d591bd',
'type': 'video'}}
""" """
log.debug('Processing: %s' % task) log.debug('Processing: %s' % task)
data = task['data'] data = task['data']
if task['action'] == 'playlist': if (task['action'] == 'playlist' and
data.get('address') == 'node.plexapp.com'):
# E.g. watch later initiated by Companion
thread = Thread(target=Plex_Node,
args=('{server}%s' % data.get('key'),
data.get('offset'),
data.get('type'),
True),)
thread.setDaemon(True)
thread.start()
elif task['action'] == 'playlist':
# Get the playqueue ID
try: try:
_, queueId, query = ParseContainerKey(data['containerKey']) _, ID, query = ParseContainerKey(data['containerKey'])
except Exception as e: except Exception as e:
log.error('Exception while processing: %s' % e) log.error('Exception while processing: %s' % e)
import traceback import traceback
log.error("Traceback:\n%s" % traceback.format_exc()) log.error("Traceback:\n%s" % traceback.format_exc())
return return
if self.playlist is not None: playqueue = self.mgr.playqueue.get_playqueue_from_type(
if self.playlist.Typus() != data.get('type'): KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[data['type']])
log.debug('Switching to Kodi playlist of type %s' self.mgr.playqueue.update_playqueue_from_PMS(
% data.get('type')) playqueue,
self.playlist = None ID,
if self.playlist is None: repeat=query.get('repeat'),
if data.get('type') == 'music': offset=data.get('offset'))
self.playlist = playlist.Playlist('music')
else:
self.playlist = playlist.Playlist('video')
if queueId != self.playlist.QueueId():
log.info('New playlist received, updating!')
xml = GetPlayQueue(queueId)
if xml in (None, 401):
log.error('Could not download Plex playlist.')
return
# Clear existing playlist on the Kodi side
self.playlist.clear()
# Set new values
self.playlist.QueueId(queueId)
self.playlist.PlayQueueVersion(int(
xml.attrib.get('playQueueVersion')))
self.playlist.Guid(xml.attrib.get('guid'))
items = []
for item in xml:
items.append({
'playQueueItemID': item.get('playQueueItemID'),
'plexId': item.get('ratingKey'),
'kodiId': None})
self.playlist.playAll(
items,
startitem=self._getStartItem(data.get('key', '')),
offset=ConvertPlexToKodiTime(data.get('offset', 0)))
log.info('Initiated playlist no %s with version %s'
% (self.playlist.QueueId(),
self.playlist.PlayQueueVersion()))
else:
log.error('This has never happened before!')
def run(self): def run(self):
httpd = False httpd = False
@ -130,7 +113,7 @@ class PlexCompanion(threading.Thread):
requestMgr = httppersist.RequestMgr() requestMgr = httppersist.RequestMgr()
jsonClass = functions.jsonClass(requestMgr, self.settings) jsonClass = functions.jsonClass(requestMgr, self.settings)
subscriptionManager = subscribers.SubscriptionManager( subscriptionManager = subscribers.SubscriptionManager(
jsonClass, requestMgr, self.player, self.playlist) jsonClass, requestMgr, self.player, self.mgr)
queue = Queue.Queue(maxsize=100) queue = Queue.Queue(maxsize=100)
@ -151,9 +134,10 @@ class PlexCompanion(threading.Thread):
break break
except: except:
log.error("Unable to start PlexCompanion. Traceback:") log.error("Unable to start PlexCompanion. Traceback:")
import traceback
log.error(traceback.print_exc()) log.error(traceback.print_exc())
xbmc.sleep(3000) sleep(3000)
if start_count == 3: if start_count == 3:
log.error("Error: Unable to start web helper.") log.error("Error: Unable to start web helper.")
@ -168,7 +152,7 @@ class PlexCompanion(threading.Thread):
message_count = 0 message_count = 0
if httpd: if httpd:
t = threading.Thread(target=httpd.handle_request) t = Thread(target=httpd.handle_request)
while not threadStopped(): while not threadStopped():
# If we are not authorized, sleep # If we are not authorized, sleep
@ -177,13 +161,13 @@ class PlexCompanion(threading.Thread):
while threadSuspended(): while threadSuspended():
if threadStopped(): if threadStopped():
break break
xbmc.sleep(1000) sleep(1000)
try: try:
message_count += 1 message_count += 1
if httpd: if httpd:
if not t.isAlive(): if not t.isAlive():
# Use threads cause the method will stall # Use threads cause the method will stall
t = threading.Thread(target=httpd.handle_request) t = Thread(target=httpd.handle_request)
t.start() t.start()
if message_count == 3000: if message_count == 3000:
@ -202,6 +186,7 @@ class PlexCompanion(threading.Thread):
message_count = 0 message_count = 0
except: except:
log.warn("Error in loop, continuing anyway. Traceback:") log.warn("Error in loop, continuing anyway. Traceback:")
import traceback
log.warn(traceback.format_exc()) log.warn(traceback.format_exc())
# See if there's anything we need to process # See if there's anything we need to process
try: try:
@ -214,12 +199,12 @@ class PlexCompanion(threading.Thread):
queue.task_done() queue.task_done()
# Don't sleep # Don't sleep
continue continue
xbmc.sleep(20) sleep(20)
client.stop_all() client.stop_all()
if httpd: if httpd:
try: try:
httpd.socket.shutdown(socket.SHUT_RDWR) httpd.socket.shutdown(SHUT_RDWR)
except: except:
pass pass
finally: finally:

View file

@ -20,46 +20,118 @@ addonName = 'PlexKodiConnect'
# Multiply Plex time by this factor to receive Kodi time # Multiply Plex time by this factor to receive Kodi time
PLEX_TO_KODI_TIMEFACTOR = 1.0 / 1000.0 PLEX_TO_KODI_TIMEFACTOR = 1.0 / 1000.0
# Possible output of Kodi's ListItem.DBTYPE for all video items
# All the Plex types as communicated in the PMS xml replies
PLEX_TYPE_VIDEO = 'video'
PLEX_TYPE_MOVIE = 'movie'
PLEX_TYPE_CLIP = 'clip' # e.g. trailers
PLEX_TYPE_EPISODE = 'episode'
PLEX_TYPE_SEASON = 'season'
PLEX_TYPE_SHOW = 'show'
PLEX_TYPE_AUDIO = 'music'
PLEX_TYPE_SONG = 'track'
PLEX_TYPE_ALBUM = 'album'
PLEX_TYPE_ARTIST = 'artist'
PLEX_TYPE_PHOTO = 'photo'
# All the Kodi types as e.g. used in the JSON API
KODI_TYPE_VIDEO = 'video'
KODI_TYPE_MOVIE = 'movie'
KODI_TYPE_SET = 'set' # for movie sets of several movies
KODI_TYPE_CLIP = 'clip' # e.g. trailers
KODI_TYPE_EPISODE = 'episode'
KODI_TYPE_SEASON = 'season'
KODI_TYPE_SHOW = 'tvshow'
KODI_TYPE_AUDIO = 'audio'
KODI_TYPE_SONG = 'song'
KODI_TYPE_ALBUM = 'album'
KODI_TYPE_ARTIST = 'artist'
KODI_TYPE_PHOTO = 'photo'
# Translation tables
KODI_VIDEOTYPES = ( KODI_VIDEOTYPES = (
'video', KODI_TYPE_VIDEO,
'movie', KODI_TYPE_MOVIE,
'set', KODI_TYPE_SHOW,
'tvshow', KODI_TYPE_SEASON,
'season', KODI_TYPE_EPISODE,
'episode', KODI_TYPE_SET
'musicvideo'
) )
# Possible output of Kodi's ListItem.DBTYPE for all audio items
KODI_AUDIOTYPES = ( KODI_AUDIOTYPES = (
'music', KODI_TYPE_SONG,
'song', KODI_TYPE_ALBUM,
'album', KODI_TYPE_ARTIST,
'artist'
) )
ITEMTYPE_FROM_PLEXTYPE = { ITEMTYPE_FROM_PLEXTYPE = {
'movie': 'Movies', PLEX_TYPE_MOVIE: 'Movies',
'season': 'TVShows', PLEX_TYPE_SEASON: 'TVShows',
'episode': 'TVShows', KODI_TYPE_EPISODE: 'TVShows',
'show': 'TVShows', PLEX_TYPE_SHOW: 'TVShows',
'artist': 'Music', PLEX_TYPE_ARTIST: 'Music',
'album': 'Music', PLEX_TYPE_ALBUM: 'Music',
'track': 'Music', PLEX_TYPE_SONG: 'Music',
'song': 'Music' }
ITEMTYPE_FROM_KODITYPE = {
KODI_TYPE_MOVIE: 'Movies',
KODI_TYPE_SEASON: 'TVShows',
KODI_TYPE_EPISODE: 'TVShows',
KODI_TYPE_SHOW: 'TVShows',
KODI_TYPE_ARTIST: 'Music',
KODI_TYPE_ALBUM: 'Music',
KODI_TYPE_SONG: 'Music',
} }
KODITYPE_FROM_PLEXTYPE = { KODITYPE_FROM_PLEXTYPE = {
'movie': 'movie', PLEX_TYPE_MOVIE: KODI_TYPE_MOVIE,
'episode': 'episode', PLEX_TYPE_EPISODE: KODI_TYPE_EPISODE,
'track': 'song', PLEX_TYPE_SEASON: KODI_TYPE_SEASON,
'artist': 'artist', PLEX_TYPE_SHOW: KODI_TYPE_SHOW,
'album': 'album', PLEX_TYPE_SONG: KODI_TYPE_SONG,
PLEX_TYPE_ARTIST: KODI_TYPE_ARTIST,
PLEX_TYPE_ALBUM: KODI_TYPE_ALBUM,
PLEX_TYPE_PHOTO: KODI_TYPE_PHOTO,
'XXXXXX': 'musicvideo', 'XXXXXX': 'musicvideo',
'XXXXXXX': 'genre' 'XXXXXXX': 'genre'
} }
KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE = {
PLEX_TYPE_VIDEO: KODI_TYPE_VIDEO,
PLEX_TYPE_MOVIE: KODI_TYPE_VIDEO,
PLEX_TYPE_EPISODE: KODI_TYPE_VIDEO,
PLEX_TYPE_SEASON: KODI_TYPE_VIDEO,
PLEX_TYPE_SHOW: KODI_TYPE_VIDEO,
PLEX_TYPE_CLIP: KODI_TYPE_VIDEO,
PLEX_TYPE_ARTIST: KODI_TYPE_AUDIO,
PLEX_TYPE_ALBUM: KODI_TYPE_AUDIO,
PLEX_TYPE_SONG: KODI_TYPE_AUDIO,
PLEX_TYPE_AUDIO: KODI_TYPE_AUDIO
}
REMAP_TYPE_FROM_PLEXTYPE = {
PLEX_TYPE_MOVIE: 'movie',
PLEX_TYPE_CLIP: 'clip',
PLEX_TYPE_SHOW: 'tv',
PLEX_TYPE_SEASON: 'tv',
PLEX_TYPE_EPISODE: 'tv',
PLEX_TYPE_ARTIST: 'music',
PLEX_TYPE_ALBUM: 'music',
PLEX_TYPE_SONG: 'music',
PLEX_TYPE_PHOTO: 'photo'
}
REMAP_TYPE_FROM_PLEXTYPE = { REMAP_TYPE_FROM_PLEXTYPE = {
'movie': 'movie', 'movie': 'movie',
@ -159,22 +231,6 @@ def SelectStreams(url, args):
url + '?' + urlencode(args), action_type='PUT') url + '?' + urlencode(args), action_type='PUT')
def GetPlayQueue(playQueueID):
"""
Fetches the PMS playqueue with the playQueueID as an XML
Returns None if something went wrong
"""
url = "{server}/playQueues/%s" % playQueueID
args = {'Accept': 'application/xml'}
xml = downloadutils.DownloadUtils().downloadUrl(url, headerOptions=args)
try:
xml.attrib['playQueueID']
except (AttributeError, KeyError):
return None
return xml
def GetPlexMetadata(key): def GetPlexMetadata(key):
""" """
Returns raw API metadata for key as an etree XML. Returns raw API metadata for key as an etree XML.
@ -388,23 +444,22 @@ def GetPlexCollections(mediatype):
return collections return collections
def GetPlexPlaylist(itemid, librarySectionUUID, mediatype='movie'): def GetPlexPlaylist(itemid, librarySectionUUID, mediatype='movie',
trailers=False):
""" """
Returns raw API metadata XML dump for a playlist with e.g. trailers. Returns raw API metadata XML dump for a playlist with e.g. trailers.
""" """
trailerNumber = settings('trailerNumber')
if not trailerNumber:
trailerNumber = '3'
url = "{server}/playQueues" url = "{server}/playQueues"
args = { args = {
'type': mediatype, 'type': mediatype,
'uri': ('library://' + librarySectionUUID + 'uri': ('library://' + librarySectionUUID +
'/item/%2Flibrary%2Fmetadata%2F' + itemid), '/item/%2Flibrary%2Fmetadata%2F' + itemid),
'includeChapters': '1', 'includeChapters': '1',
'extrasPrefixCount': trailerNumber,
'shuffle': '0', 'shuffle': '0',
'repeat': '0' 'repeat': '0'
} }
if trailers is True:
args['extrasPrefixCount'] = settings('trailerNumber')
xml = downloadutils.DownloadUtils().downloadUrl( xml = downloadutils.DownloadUtils().downloadUrl(
url + '?' + urlencode(args), action_type="POST") url + '?' + urlencode(args), action_type="POST")
try: try:

View file

@ -281,7 +281,7 @@ class Artwork():
def cacheTexture(self, url): def cacheTexture(self, url):
# Cache a single image url to the texture cache # Cache a single image url to the texture cache
if url and self.enableTextureCache: if url and self.enableTextureCache:
self.queue.put(double_urlencode(url)) self.queue.put(double_urlencode(tryEncode(url)))
def addArtwork(self, artwork, kodiId, mediaType, cursor): def addArtwork(self, artwork, kodiId, mediaType, cursor):
# Kodi conversion table # Kodi conversion table

View file

@ -49,7 +49,7 @@ class ClientInfo():
'X-Plex-Product': self.getAddonName(), 'X-Plex-Product': self.getAddonName(),
'X-Plex-Version': self.getVersion(), 'X-Plex-Version': self.getVersion(),
'X-Plex-Client-Identifier': self.getDeviceId(), 'X-Plex-Client-Identifier': self.getDeviceId(),
'X-Plex-Provides': 'player', 'X-Plex-Provides': 'client,controller,player',
} }
if window('pms_token'): if window('pms_token'):

View file

@ -8,7 +8,7 @@ import xbmc
import xbmcaddon import xbmcaddon
import PlexFunctions as PF import PlexFunctions as PF
import embydb_functions as embydb import plexdb_functions as plexdb
from utils import window, settings, dialog, language as lang, kodiSQL from utils import window, settings, dialog, language as lang, kodiSQL
from dialogs import context from dialogs import context
@ -75,8 +75,8 @@ class ContextMenu(object):
def _get_item_id(cls, kodi_id, item_type): def _get_item_id(cls, kodi_id, item_type):
item_id = xbmc.getInfoLabel('ListItem.Property(plexid)') item_id = xbmc.getInfoLabel('ListItem.Property(plexid)')
if not item_id and kodi_id and item_type: if not item_id and kodi_id and item_type:
with embydb.GetEmbyDB() as emby_db: with plexdb.Get_Plex_DB() as plexcursor:
item = emby_db.getItem_byKodiId(kodi_id, item_type) item = plexcursor.getItem_byKodiId(kodi_id, item_type)
try: try:
item_id = item[0] item_id = item[0]
except TypeError: except TypeError:
@ -140,8 +140,8 @@ class ContextMenu(object):
elif selected == OPTIONS['PMS_Play']: elif selected == OPTIONS['PMS_Play']:
self._PMS_play() self._PMS_play()
elif selected == OPTIONS['Refresh']: # elif selected == OPTIONS['Refresh']:
self.emby.refreshItem(self.item_id) # self.emby.refreshItem(self.item_id)
# elif selected == OPTIONS['AddFav']: # elif selected == OPTIONS['AddFav']:
# self.emby.updateUserRating(self.item_id, favourite=True) # self.emby.updateUserRating(self.item_id, favourite=True)
@ -212,6 +212,6 @@ class ContextMenu(object):
'mode': "play" 'mode': "play"
} }
from urllib import urlencode from urllib import urlencode
handle = ("plugin://plugin.video.plexkodiconnect.movies?%s" handle = ("plugin://plugin.video.plexkodiconnect/movies?%s"
% urlencode(params)) % urlencode(params))
xbmc.executebuiltin('RunPlugin(%s)' % handle) xbmc.executebuiltin('RunPlugin(%s)' % handle)

View file

@ -67,7 +67,7 @@ class UsersConnect(xbmcgui.WindowXMLDialog):
if self.kodi_version > 15: if self.kodi_version > 15:
item.setArt({'Icon': user_image}) item.setArt({'Icon': user_image})
else: else:
item.setIconImage(user_image) item.setArt({'icon': user_image})
return item return item

View file

@ -1,402 +0,0 @@
# -*- coding: utf-8 -*-
###############################################################################
from utils import kodiSQL
import logging
###############################################################################
log = logging.getLogger("PLEX."+__name__)
###############################################################################
class GetEmbyDB():
"""
Usage: with GetEmbyDB() as emby_db:
do stuff with emby_db
On exiting "with" (no matter what), commits get automatically committed
and the db gets closed
"""
def __enter__(self):
self.embyconn = kodiSQL('emby')
self.emby_db = Embydb_Functions(self.embyconn.cursor())
return self.emby_db
def __exit__(self, type, value, traceback):
self.embyconn.commit()
self.embyconn.close()
class Embydb_Functions():
def __init__(self, embycursor):
self.embycursor = embycursor
def getViews(self):
views = []
query = ' '.join((
"SELECT view_id",
"FROM view"
))
self.embycursor.execute(query)
rows = self.embycursor.fetchall()
for row in rows:
views.append(row[0])
return views
def getAllViewInfo(self):
embycursor = self.embycursor
views = []
query = ' '.join((
"SELECT view_id, view_name, media_type",
"FROM view"
))
embycursor.execute(query)
rows = embycursor.fetchall()
for row in rows:
views.append({'id': row[0],
'name': row[1],
'itemtype': row[2]})
return views
def getView_byId(self, viewid):
query = ' '.join((
"SELECT view_name, media_type, kodi_tagid",
"FROM view",
"WHERE view_id = ?"
))
self.embycursor.execute(query, (viewid,))
view = self.embycursor.fetchone()
return view
def getView_byType(self, mediatype):
views = []
query = ' '.join((
"SELECT view_id, view_name, media_type",
"FROM view",
"WHERE media_type = ?"
))
self.embycursor.execute(query, (mediatype,))
rows = self.embycursor.fetchall()
for row in rows:
views.append({
'id': row[0],
'name': row[1],
'itemtype': row[2]
})
return views
def getView_byName(self, tagname):
query = ' '.join((
"SELECT view_id",
"FROM view",
"WHERE view_name = ?"
))
self.embycursor.execute(query, (tagname,))
try:
view = self.embycursor.fetchone()[0]
except TypeError:
view = None
return view
def addView(self, plexid, name, mediatype, tagid):
query = (
'''
INSERT INTO view(
view_id, view_name, media_type, kodi_tagid)
VALUES (?, ?, ?, ?)
'''
)
self.embycursor.execute(query, (plexid, name, mediatype, tagid))
def updateView(self, name, tagid, mediafolderid):
query = ' '.join((
"UPDATE view",
"SET view_name = ?, kodi_tagid = ?",
"WHERE view_id = ?"
))
self.embycursor.execute(query, (name, tagid, mediafolderid))
def removeView(self, viewid):
query = ' '.join((
"DELETE FROM view",
"WHERE view_id = ?"
))
self.embycursor.execute(query, (viewid,))
def getItem_byFileId(self, fileId, kodiType):
"""
Returns the Plex itemId by using the Kodi fileId. VIDEO ONLY
kodiType: 'movie', 'episode', ...
"""
query = ' '.join((
"SELECT emby_id",
"FROM emby",
"WHERE kodi_fileid = ? AND media_type = ?"
))
try:
self.embycursor.execute(query, (fileId, kodiType))
item = self.embycursor.fetchone()[0]
return item
except:
return None
def getMusicItem_byFileId(self, fileId, kodiType):
"""
Returns the Plex itemId by using the Kodi fileId. MUSIC ONLY
kodiType: 'song'
"""
query = ' '.join((
"SELECT emby_id",
"FROM emby",
"WHERE kodi_id = ? AND media_type = ?"
))
try:
self.embycursor.execute(query, (fileId, kodiType))
item = self.embycursor.fetchone()[0]
return item
except:
return None
def getItem_byId(self, plexid):
query = ' '.join((
"SELECT kodi_id, kodi_fileid, kodi_pathid, parent_id, media_type, emby_type",
"FROM emby",
"WHERE emby_id = ?"
))
try:
self.embycursor.execute(query, (plexid,))
item = self.embycursor.fetchone()
return item
except: return None
def getItem_byWildId(self, plexid):
query = ' '.join((
"SELECT kodi_id, media_type",
"FROM emby",
"WHERE emby_id LIKE ?"
))
self.embycursor.execute(query, (plexid+"%",))
return self.embycursor.fetchall()
def getItem_byView(self, mediafolderid):
query = ' '.join((
"SELECT kodi_id",
"FROM emby",
"WHERE media_folder = ?"
))
self.embycursor.execute(query, (mediafolderid,))
return self.embycursor.fetchall()
def getPlexId(self, kodiid, mediatype):
"""
Returns the Plex ID usind the Kodiid. Result:
(Plex Id, Parent's Plex Id)
"""
query = ' '.join((
"SELECT emby_id, parent_id",
"FROM emby",
"WHERE kodi_id = ? AND media_type = ?"
))
try:
self.embycursor.execute(query, (kodiid, mediatype))
item = self.embycursor.fetchone()
return item
except:
return None
def getItem_byKodiId(self, kodiid, mediatype):
query = ' '.join((
"SELECT emby_id, parent_id",
"FROM emby",
"WHERE kodi_id = ?",
"AND media_type = ?"
))
self.embycursor.execute(query, (kodiid, mediatype,))
return self.embycursor.fetchone()
def getItem_byParentId(self, parentid, mediatype):
query = ' '.join((
"SELECT emby_id, kodi_id, kodi_fileid",
"FROM emby",
"WHERE parent_id = ?",
"AND media_type = ?"
))
self.embycursor.execute(query, (parentid, mediatype,))
return self.embycursor.fetchall()
def getItemId_byParentId(self, parentid, mediatype):
query = ' '.join((
"SELECT emby_id, kodi_id",
"FROM emby",
"WHERE parent_id = ?",
"AND media_type = ?"
))
self.embycursor.execute(query, (parentid, mediatype,))
return self.embycursor.fetchall()
def getChecksum(self, mediatype):
query = ' '.join((
"SELECT emby_id, checksum",
"FROM emby",
"WHERE emby_type = ?"
))
self.embycursor.execute(query, (mediatype,))
return self.embycursor.fetchall()
def getMediaType_byId(self, plexid):
query = ' '.join((
"SELECT emby_type",
"FROM emby",
"WHERE emby_id = ?"
))
self.embycursor.execute(query, (plexid,))
try:
itemtype = self.embycursor.fetchone()[0]
except TypeError:
itemtype = None
return itemtype
def sortby_mediaType(self, itemids, unsorted=True):
sorted_items = {}
for itemid in itemids:
mediatype = self.getMediaType_byId(itemid)
if mediatype:
sorted_items.setdefault(mediatype, []).append(itemid)
elif unsorted:
sorted_items.setdefault('Unsorted', []).append(itemid)
return sorted_items
def addReference(self, plexid, kodiid, embytype, mediatype, fileid=None, pathid=None,
parentid=None, checksum=None, mediafolderid=None):
query = (
'''
INSERT OR REPLACE INTO emby(
emby_id, kodi_id, kodi_fileid, kodi_pathid, emby_type, media_type, parent_id,
checksum, media_folder)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
'''
)
self.embycursor.execute(query, (plexid, kodiid, fileid, pathid, embytype, mediatype,
parentid, checksum, mediafolderid))
def updateReference(self, plexid, checksum):
query = "UPDATE emby SET checksum = ? WHERE emby_id = ?"
self.embycursor.execute(query, (checksum, plexid))
def updateParentId(self, plexid, parent_kodiid):
query = "UPDATE emby SET parent_id = ? WHERE emby_id = ?"
self.embycursor.execute(query, (parent_kodiid, plexid))
def removeItems_byParentId(self, parent_kodiid, mediatype):
query = ' '.join((
"DELETE FROM emby",
"WHERE parent_id = ?",
"AND media_type = ?"
))
self.embycursor.execute(query, (parent_kodiid, mediatype,))
def removeItem_byKodiId(self, kodiid, mediatype):
query = ' '.join((
"DELETE FROM emby",
"WHERE kodi_id = ?",
"AND media_type = ?"
))
self.embycursor.execute(query, (kodiid, mediatype,))
def removeItem(self, plexid):
query = "DELETE FROM emby WHERE emby_id = ?"
self.embycursor.execute(query, (plexid,))
def removeWildItem(self, plexid):
query = "DELETE FROM emby WHERE emby_id LIKE ?"
self.embycursor.execute(query, (plexid+"%",))
def itemsByType(self, plextype):
"""
Returns a list of dictionaries for all Kodi DB items present for
plextype. One dict is of the type
{
'plexId': the Plex id
'kodiId': the Kodi id
'kodi_type': e.g. 'movie', 'tvshow'
'plex_type': e.g. 'Movie', 'Series', the input plextype
}
"""
query = ' '.join((
"SELECT emby_id, kodi_id, media_type",
"FROM emby",
"WHERE emby_type = ?",
))
self.embycursor.execute(query, (plextype, ))
result = []
for row in self.embycursor.fetchall():
result.append({
'plexId': row[0],
'kodiId': row[1],
'kodi_type': row[2],
'plex_type': plextype
})
return result

View file

@ -17,12 +17,13 @@ from utils import window, settings, language as lang
from utils import tryDecode, tryEncode, CatchExceptions from utils import tryDecode, tryEncode, CatchExceptions
import clientinfo import clientinfo
import downloadutils import downloadutils
import embydb_functions as embydb import plexdb_functions as plexdb
import playbackutils as pbutils import playbackutils as pbutils
import playlist
import PlexFunctions import PlexFunctions
import PlexAPI import PlexAPI
from PKC_listitem import convert_PKC_to_listitem
from playqueue import Playqueue
############################################################################### ###############################################################################
@ -33,40 +34,6 @@ addonName = "PlexKodiConnect"
############################################################################### ###############################################################################
def plexCompanion(fullurl, params):
params = PlexFunctions.LiteralEval(params[26:])
if params['machineIdentifier'] != window('plex_machineIdentifier'):
log.error("Command was not for us, machineIdentifier controller: %s, "
"our machineIdentifier : %s"
% (params['machineIdentifier'],
window('plex_machineIdentifier')))
return
library, key, query = PlexFunctions.ParseContainerKey(
params['containerKey'])
# Construct a container key that works always (get rid of playlist args)
window('containerKey', '/'+library+'/'+key)
if 'playQueues' in library:
log.debug("Playing a playQueue. Query was: %s" % query)
# Playing a playlist that we need to fetch from PMS
xml = PlexFunctions.GetPlayQueue(key)
if xml is None:
log.error("Error getting PMS playlist for key %s" % key)
return
else:
resume = PlexFunctions.ConvertPlexToKodiTime(
params.get('offset', 0))
itemids = []
for item in xml:
itemids.append(item.get('ratingKey'))
return playlist.Playlist().playAll(itemids, resume)
else:
log.error("Not knowing what to do for now - no playQueue sent")
def chooseServer(): def chooseServer():
""" """
Lets user choose from list of PMS Lets user choose from list of PMS
@ -130,45 +97,21 @@ def togglePlexTV():
sound=False) sound=False)
def PassPlaylist(xml, resume=None): def Plex_Node(url, viewOffset, plex_type, playdirectly=False):
"""
resume in KodiTime - seconds.
"""
# Set window properties to make them available later for other threads
windowArgs = [
# 'containerKey'
'playQueueID',
'playQueueVersion']
for arg in windowArgs:
window(arg, value=xml.attrib.get(arg))
# Get resume point
from utils import IntFromStr
resume1 = PlexFunctions.ConvertPlexToKodiTime(IntFromStr(
xml.attrib.get('playQueueSelectedItemOffset', 0)))
resume2 = resume
resume = max(resume1, resume2)
pbutils.PlaybackUtils(xml).StartPlay(
resume=resume,
resumeId=xml.attrib.get('playQueueSelectedItemID', None))
def playWatchLater(itemid, viewOffset):
""" """
Called only for a SINGLE element for Plex.tv watch later Called only for a SINGLE element for Plex.tv watch later
Always to return with a "setResolvedUrl" Always to return with a "setResolvedUrl"
""" """
log.info('playWatchLater called with id: %s, viewOffset: %s' log.info('Plex_Node called with url: %s, viewOffset: %s'
% (itemid, viewOffset)) % (url, viewOffset))
# Plex redirect, e.g. watch later. Need to get actual URLs # Plex redirect, e.g. watch later. Need to get actual URLs
xml = downloadutils.DownloadUtils().downloadUrl(itemid, xml = downloadutils.DownloadUtils().downloadUrl(url)
authenticate=False) try:
if xml in (None, 401): xml[0].attrib
log.error("Could not resolve url %s" % itemid) except:
return xbmcplugin.setResolvedUrl( log.error('Could not download PMS metadata')
int(sys.argv[1]), False, xbmcgui.ListItem()) return
if viewOffset != '0': if viewOffset != '0':
try: try:
viewOffset = int(PlexFunctions.PLEX_TO_KODI_TIMEFACTOR * viewOffset = int(PlexFunctions.PLEX_TO_KODI_TIMEFACTOR *
@ -178,41 +121,20 @@ def playWatchLater(itemid, viewOffset):
else: else:
window('plex_customplaylist.seektime', value=str(viewOffset)) window('plex_customplaylist.seektime', value=str(viewOffset))
log.info('Set resume point to %s' % str(viewOffset)) log.info('Set resume point to %s' % str(viewOffset))
return pbutils.PlaybackUtils(xml).play(None, 'plexnode') typus = PlexFunctions.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[plex_type]
playqueue = Playqueue().get_playqueue_from_type(typus)
result = pbutils.PlaybackUtils(xml, playqueue).play(
def doPlayback(itemid, dbid): None,
""" kodi_id='plexnode',
Called only for a SINGLE element, not playQueues plex_lib_UUID=xml.attrib.get('librarySectionUUID'))
if result.listitem:
Always to return with a "setResolvedUrl" listitem = convert_PKC_to_listitem(result.listitem)
"""
if window('plex_authenticated') != "true":
log.error('Not yet authenticated for a PMS, abort starting playback')
# Not yet connected to a PMS server
xbmcgui.Dialog().notification(
addonName,
lang(39210),
xbmcgui.NOTIFICATION_ERROR,
7000,
True)
return xbmcplugin.setResolvedUrl(
int(sys.argv[1]), False, xbmcgui.ListItem())
xml = PlexFunctions.GetPlexMetadata(itemid)
if xml in (None, 401):
return xbmcplugin.setResolvedUrl(
int(sys.argv[1]), False, xbmcgui.ListItem())
if xml[0].attrib.get('type') == 'photo':
# Photo
API = PlexAPI.API(xml[0])
listitem = API.CreateListItemFromPlexItem()
API.AddStreamInfo(listitem)
pbutils.PlaybackUtils(xml[0]).setArtwork(listitem)
return xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem)
else: else:
# Video return
return pbutils.PlaybackUtils(xml).play(itemid, dbid) if playdirectly:
xbmc.Player().play(listitem.getfilename(), listitem)
else:
xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem)
##### DO RESET AUTH ##### ##### DO RESET AUTH #####
@ -319,12 +241,8 @@ def deleteItem():
log.error("Unknown type, unable to proceed.") log.error("Unknown type, unable to proceed.")
return return
from utils import kodiSQL with plexdb.Get_Plex_DB() as plexcursor:
embyconn = kodiSQL('emby') item = plexcursor.getItem_byKodiId(dbid, itemtype)
embycursor = embyconn.cursor()
emby_db = embydb.Embydb_Functions(embycursor)
item = emby_db.getItem_byKodiId(dbid, itemtype)
embycursor.close()
try: try:
plexid = item[0] plexid = item[0]
@ -467,99 +385,6 @@ def BrowseContent(viewname, browse_type="", folderid=""):
xbmcplugin.endOfDirectory(handle=int(sys.argv[1])) xbmcplugin.endOfDirectory(handle=int(sys.argv[1]))
##### CREATE LISTITEM FROM EMBY METADATA #####
# def createListItemFromEmbyItem(item,art=artwork.Artwork(),doUtils=downloadutils.DownloadUtils()):
def createListItemFromEmbyItem(item,art=None,doUtils=downloadutils.DownloadUtils()):
API = PlexAPI.API(item)
itemid = item['Id']
title = item.get('Name')
li = xbmcgui.ListItem(title)
premieredate = item.get('PremiereDate',"")
if not premieredate: premieredate = item.get('DateCreated',"")
if premieredate:
premieredatelst = premieredate.split('T')[0].split("-")
premieredate = "%s.%s.%s" %(premieredatelst[2],premieredatelst[1],premieredatelst[0])
li.setProperty("plexid",itemid)
allart = art.getAllArtwork(item)
if item["Type"] == "Photo":
#listitem setup for pictures...
img_path = allart.get('Primary')
li.setProperty("path",img_path)
picture = doUtils.downloadUrl("{server}/Items/%s/Images" %itemid)
if picture:
picture = picture[0]
if picture.get("Width") > picture.get("Height"):
li.setArt( {"fanart": img_path}) #add image as fanart for use with skinhelper auto thumb/backgrund creation
li.setInfo('pictures', infoLabels={ "picturepath": img_path, "date": premieredate, "size": picture.get("Size"), "exif:width": str(picture.get("Width")), "exif:height": str(picture.get("Height")), "title": title})
li.setThumbnailImage(img_path)
li.setProperty("plot",API.getOverview())
li.setIconImage('DefaultPicture.png')
else:
#normal video items
li.setProperty('IsPlayable', 'true')
path = "%s?id=%s&mode=play" % (sys.argv[0], item.get("Id"))
li.setProperty("path",path)
genre = API.getGenres()
overlay = 0
userdata = API.getUserData()
runtime = item.get("RunTimeTicks",0)/ 10000000.0
seektime = userdata['Resume']
if seektime:
li.setProperty("resumetime", str(seektime))
li.setProperty("totaltime", str(runtime))
played = userdata['Played']
if played: overlay = 7
else: overlay = 6
playcount = userdata['PlayCount']
if playcount is None:
playcount = 0
rating = item.get('CommunityRating')
if not rating: rating = userdata['UserRating']
# Populate the extradata list and artwork
extradata = {
'id': itemid,
'rating': rating,
'year': item.get('ProductionYear'),
'genre': genre,
'playcount': str(playcount),
'title': title,
'plot': API.getOverview(),
'Overlay': str(overlay),
'duration': runtime
}
if premieredate:
extradata["premieredate"] = premieredate
extradata["date"] = premieredate
li.setInfo('video', infoLabels=extradata)
if allart.get('Primary'):
li.setThumbnailImage(allart.get('Primary'))
else: li.setThumbnailImage('DefaultTVShows.png')
li.setIconImage('DefaultTVShows.png')
if not allart.get('Background'): #add image as fanart for use with skinhelper auto thumb/backgrund creation
li.setArt( {"fanart": allart.get('Primary') } )
else:
pbutils.PlaybackUtils(item).setArtwork(li)
mediastreams = API.getMediaStreams()
videostreamFound = False
if mediastreams:
for key, value in mediastreams.iteritems():
if key == "video" and value: videostreamFound = True
if value: li.addStreamInfo(key, value[0])
if not videostreamFound:
#just set empty streamdetails to prevent errors in the logs
li.addStreamInfo("video", {'duration': runtime})
return li
##### BROWSE EMBY CHANNELS ##### ##### BROWSE EMBY CHANNELS #####
def BrowseChannels(itemid, folderid=None): def BrowseChannels(itemid, folderid=None):
@ -664,7 +489,7 @@ def createListItem(item, appendShowTitle=False, appendSxxExx=False):
li.setProperty('totaltime', str(item['resume']['total'])) li.setProperty('totaltime', str(item['resume']['total']))
li.setArt(item['art']) li.setArt(item['art'])
li.setThumbnailImage(item['art'].get('thumb','')) li.setThumbnailImage(item['art'].get('thumb',''))
li.setIconImage('DefaultTVShows.png') li.setArt({'icon': 'DefaultTVShows.png'})
li.setProperty('dbid', str(item['episodeid'])) li.setProperty('dbid', str(item['episodeid']))
li.setProperty('fanart_image', item['art'].get('tvshow.fanart','')) li.setProperty('fanart_image', item['art'].get('tvshow.fanart',''))
for key, value in item['streamdetails'].iteritems(): for key, value in item['streamdetails'].iteritems():
@ -1095,14 +920,14 @@ def BrowsePlexContent(viewid, mediatype="", folderid=""):
li.setProperty('IsPlayable', 'false') li.setProperty('IsPlayable', 'false')
path = "%s?id=%s&mode=browseplex&type=%s&folderid=%s" \ path = "%s?id=%s&mode=browseplex&type=%s&folderid=%s" \
% (sys.argv[0], viewid, mediatype, API.getKey()) % (sys.argv[0], viewid, mediatype, API.getKey())
pbutils.PlaybackUtils(item).setArtwork(li) API.set_listitem_artwork(li)
xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]),
url=path, url=path,
listitem=li, listitem=li,
isFolder=True) isFolder=True)
else: else:
li = API.CreateListItemFromPlexItem() li = API.CreateListItemFromPlexItem()
pbutils.PlaybackUtils(item).setArtwork(li) API.set_listitem_artwork(li)
xbmcplugin.addDirectoryItem( xbmcplugin.addDirectoryItem(
handle=int(sys.argv[1]), handle=int(sys.argv[1]),
url=li.getProperty("path"), url=li.getProperty("path"),
@ -1159,7 +984,7 @@ def getOnDeck(viewid, mediatype, tagname, limit):
appendShowTitle=appendShowTitle, appendShowTitle=appendShowTitle,
appendSxxExx=appendSxxExx) appendSxxExx=appendSxxExx)
API.AddStreamInfo(listitem) API.AddStreamInfo(listitem)
pbutils.PlaybackUtils(item).setArtwork(listitem) API.set_listitem_artwork(listitem)
if directpaths: if directpaths:
url = API.getFilePath() url = API.getFilePath()
else: else:
@ -1168,7 +993,7 @@ def getOnDeck(viewid, mediatype, tagname, limit):
'id': API.getRatingKey(), 'id': API.getRatingKey(),
'dbid': listitem.getProperty('dbid') 'dbid': listitem.getProperty('dbid')
} }
url = "plugin://plugin.video.plexkodiconnect.tvshows/?%s" \ url = "plugin://plugin.video.plexkodiconnect/tvshows/?%s" \
% urllib.urlencode(params) % urllib.urlencode(params)
xbmcplugin.addDirectoryItem( xbmcplugin.addDirectoryItem(
handle=int(sys.argv[1]), handle=int(sys.argv[1]),
@ -1306,15 +1131,16 @@ def watchlater():
xbmcplugin.setContent(int(sys.argv[1]), 'movies') xbmcplugin.setContent(int(sys.argv[1]), 'movies')
url = "plugin://plugin.video.plexkodiconnect/" url = "plugin://plugin.video.plexkodiconnect/"
params = { params = {
'mode': "playwatchlater", 'mode': "Plex_Node",
} }
for item in xml: for item in xml:
API = PlexAPI.API(item) API = PlexAPI.API(item)
listitem = API.CreateListItemFromPlexItem() listitem = API.CreateListItemFromPlexItem()
API.AddStreamInfo(listitem) API.AddStreamInfo(listitem)
pbutils.PlaybackUtils(item).setArtwork(listitem) API.set_listitem_artwork(listitem)
params['id'] = item.attrib.get('key') params['id'] = item.attrib.get('key')
params['viewOffset'] = item.attrib.get('viewOffset', '0') params['viewOffset'] = item.attrib.get('viewOffset', '0')
params['plex_type'] = item.attrib.get('type')
xbmcplugin.addDirectoryItem( xbmcplugin.addDirectoryItem(
handle=int(sys.argv[1]), handle=int(sys.argv[1]),
url="%s?%s" % (url, urllib.urlencode(params)), url="%s?%s" % (url, urllib.urlencode(params)),

View file

@ -9,7 +9,7 @@ import xbmcgui
from utils import settings, window, language as lang from utils import settings, window, language as lang
import clientinfo import clientinfo
import downloadutils import downloadutils
import userclient from userclient import UserClient
import PlexAPI import PlexAPI
from PlexFunctions import GetMachineIdentifier, get_PMS_settings from PlexFunctions import GetMachineIdentifier, get_PMS_settings
@ -30,11 +30,10 @@ class InitialSetup():
self.clientInfo = clientinfo.ClientInfo() self.clientInfo = clientinfo.ClientInfo()
self.addonId = self.clientInfo.getAddonId() self.addonId = self.clientInfo.getAddonId()
self.doUtils = downloadutils.DownloadUtils().downloadUrl self.doUtils = downloadutils.DownloadUtils().downloadUrl
self.userClient = userclient.UserClient()
self.plx = PlexAPI.PlexAPI() self.plx = PlexAPI.PlexAPI()
self.dialog = xbmcgui.Dialog() self.dialog = xbmcgui.Dialog()
self.server = self.userClient.getServer() self.server = UserClient().getServer()
self.serverid = settings('plex_machineIdentifier') self.serverid = settings('plex_machineIdentifier')
# Get Plex credentials from settings file, if they exist # Get Plex credentials from settings file, if they exist
plexdict = self.plx.GetPlexLoginFromSettings() plexdict = self.plx.GetPlexLoginFromSettings()

File diff suppressed because it is too large Load diff

View file

@ -7,6 +7,7 @@ from ntpath import dirname
import artwork import artwork
from utils import kodiSQL, KODIVERSION from utils import kodiSQL, KODIVERSION
from PlexFunctions import KODI_TYPE_MOVIE, KODI_TYPE_EPISODE
############################################################################### ###############################################################################
@ -31,8 +32,8 @@ class GetKodiDB():
def __enter__(self): def __enter__(self):
self.kodiconn = kodiSQL(self.itemType) self.kodiconn = kodiSQL(self.itemType)
self.emby_db = Kodidb_Functions(self.kodiconn.cursor()) kodi_db = Kodidb_Functions(self.kodiconn.cursor())
return self.emby_db return kodi_db
def __exit__(self, type, value, traceback): def __exit__(self, type, value, traceback):
self.kodiconn.commit() self.kodiconn.commit()
@ -61,7 +62,7 @@ class Kodidb_Functions():
self.cursor.execute( self.cursor.execute(
query, ('movies', query, ('movies',
'metadata.local', 'metadata.local',
'plugin://plugin.video.plexkodiconnect.movies%%')) 'plugin://plugin.video.plexkodiconnect/movies%%'))
def getParentPathId(self, path): def getParentPathId(self, path):
""" """
@ -869,7 +870,7 @@ class Kodidb_Functions():
self.cursor.execute(query, (idFile,)) self.cursor.execute(query, (idFile,))
try: try:
itemId = self.cursor.fetchone()[0] itemId = self.cursor.fetchone()[0]
typus = 'movie' typus = KODI_TYPE_MOVIE
except TypeError: except TypeError:
# Try tv shows next # Try tv shows next
query = ' '.join(( query = ' '.join((
@ -880,7 +881,7 @@ class Kodidb_Functions():
self.cursor.execute(query, (idFile,)) self.cursor.execute(query, (idFile,))
try: try:
itemId = self.cursor.fetchone()[0] itemId = self.cursor.fetchone()[0]
typus = 'episode' typus = KODI_TYPE_EPISODE
except TypeError: except TypeError:
log.warn('Unexpectantly did not find a match!') log.warn('Unexpectantly did not find a match!')
return return
@ -907,13 +908,13 @@ class Kodidb_Functions():
return ids return ids
def getVideoRuntime(self, kodiid, mediatype): def getVideoRuntime(self, kodiid, mediatype):
if mediatype == 'movie': if mediatype == KODI_TYPE_MOVIE:
query = ' '.join(( query = ' '.join((
"SELECT c11", "SELECT c11",
"FROM movie", "FROM movie",
"WHERE idMovie = ?", "WHERE idMovie = ?",
)) ))
elif mediatype == 'episode': elif mediatype == KODI_TYPE_EPISODE:
query = ' '.join(( query = ' '.join((
"SELECT c09", "SELECT c09",
"FROM episode", "FROM episode",
@ -1397,3 +1398,89 @@ class Kodidb_Functions():
query = "INSERT OR REPLACE INTO song_genre(idGenre, idSong) values(?, ?)" query = "INSERT OR REPLACE INTO song_genre(idGenre, idSong) values(?, ?)"
self.cursor.execute(query, (genreid, kodiid)) self.cursor.execute(query, (genreid, kodiid))
# Krypton only stuff ##############################
def create_entry_uniqueid(self):
self.cursor.execute(
"select coalesce(max(uniqueid_id),0) from uniqueid")
return self.cursor.fetchone()[0] + 1
def add_uniqueid(self, *args):
"""
Feed with:
uniqueid_id, media_id, media_type, value, type
type: e.g. 'imdb'
"""
query = '''
INSERT INTO uniqueid(
uniqueid_id, media_id, media_type, value, type)
VALUES (?, ?, ?, ?, ?)
'''
self.cursor.execute(query, (args))
def create_entry_rating(self):
self.cursor.execute("select coalesce(max(rating_id),0) from rating")
return self.cursor.fetchone()[0] + 1
def get_ratingid(self, media_id):
query = "SELECT rating_id FROM rating WHERE media_id = ?"
self.cursor.execute(query, (media_id,))
try:
ratingid = self.cursor.fetchone()[0]
except TypeError:
ratingid = None
return ratingid
def update_ratings(self, *args):
"""
Feed with media_id, media_type, rating_type, rating, votes, rating_id
"""
query = '''
UPDATE rating
SET media_id = ?,
media_type = ?,
rating_type = ?,
rating = ?,
votes = ?
WHERE rating_id = ?
'''
self.cursor.execute(query, (args))
def add_ratings(self, *args):
"""
feed with:
rating_id, media_id, media_type, rating_type, rating, votes
rating_type = 'default'
"""
query = '''
INSERT INTO rating(
rating_id, media_id, media_type, rating_type, rating, votes)
VALUES (?, ?, ?, ?, ?, ?)
'''
self.cursor.execute(query, (args))
def get_kodiid_from_filename(file):
"""
Returns the tuple (kodiid, type) if we have a video in the database with
said filename, or (None, None)
"""
kodiid = None
typus = None
try:
filename = file.rsplit('/', 1)[1]
path = file.rsplit('/', 1)[0] + '/'
except IndexError:
filename = file.rsplit('\\', 1)[1]
path = file.rsplit('\\', 1)[0] + '\\'
log.debug('Trying to figure out playing item from filename: %s '
'and path: %s' % (filename, path))
with GetKodiDB('video') as kodi_db:
try:
kodiid, typus = kodi_db.getIdFromFilename(filename, path)
except TypeError:
log.info('No kodi video element found with filename %s' % filename)
return (kodiid, typus)

View file

@ -3,17 +3,16 @@
############################################################################### ###############################################################################
import logging import logging
import json from json import loads
import xbmc from xbmc import Monitor, Player, sleep
import xbmcgui
import downloadutils import downloadutils
import embydb_functions as embydb import plexdb_functions as plexdb
import kodidb_functions as kodidb
import playbackutils as pbutils
from utils import window, settings, CatchExceptions, tryDecode, tryEncode from utils import window, settings, CatchExceptions, tryDecode, tryEncode
from PlexFunctions import scrobble, REMAP_TYPE_FROM_PLEXTYPE from PlexFunctions import scrobble, REMAP_TYPE_FROM_PLEXTYPE
from kodidb_functions import get_kodiid_from_filename
from PlexAPI import API
############################################################################### ###############################################################################
@ -22,13 +21,14 @@ log = logging.getLogger("PLEX."+__name__)
############################################################################### ###############################################################################
class KodiMonitor(xbmc.Monitor): class KodiMonitor(Monitor):
def __init__(self):
def __init__(self, callback):
self.mgr = callback
self.doUtils = downloadutils.DownloadUtils().downloadUrl self.doUtils = downloadutils.DownloadUtils().downloadUrl
self.xbmcplayer = xbmc.Player() self.xbmcplayer = Player()
xbmc.Monitor.__init__(self) self.playqueue = self.mgr.playqueue
Monitor.__init__(self)
log.info("Kodi monitor started.") log.info("Kodi monitor started.")
def onScanStarted(self, library): def onScanStarted(self, library):
@ -70,7 +70,7 @@ class KodiMonitor(xbmc.Monitor):
def onNotification(self, sender, method, data): def onNotification(self, sender, method, data):
if data: if data:
data = json.loads(data, 'utf-8') data = loads(data, 'utf-8')
log.debug("Method: %s Data: %s" % (method, data)) log.debug("Method: %s Data: %s" % (method, data))
if method == "Player.OnPlay": if method == "Player.OnPlay":
@ -92,18 +92,18 @@ class KodiMonitor(xbmc.Monitor):
log.info("Item is invalid for playstate update.") log.info("Item is invalid for playstate update.")
else: else:
# Send notification to the server. # Send notification to the server.
with embydb.GetEmbyDB() as emby_db: with plexdb.Get_Plex_DB() as plexcur:
emby_dbitem = emby_db.getItem_byKodiId(kodiid, item_type) plex_dbitem = plexcur.getItem_byKodiId(kodiid, item_type)
try: try:
itemid = emby_dbitem[0] itemid = plex_dbitem[0]
except TypeError: except TypeError:
log.error("Could not find itemid in emby database for a " log.error("Could not find itemid in plex database for a "
"video library update") "video library update")
else: else:
# Stop from manually marking as watched unwatched, with actual playback. # Stop from manually marking as watched unwatched, with actual playback.
if window('emby_skipWatched%s' % itemid) == "true": if window('plex_skipWatched%s' % itemid) == "true":
# property is set in player.py # property is set in player.py
window('emby_skipWatched%s' % itemid, clear=True) window('plex_skipWatched%s' % itemid, clear=True)
else: else:
# notify the server # notify the server
if playcount != 0: if playcount != 0:
@ -112,40 +112,7 @@ class KodiMonitor(xbmc.Monitor):
scrobble(itemid, 'unwatched') scrobble(itemid, 'unwatched')
elif method == "VideoLibrary.OnRemove": elif method == "VideoLibrary.OnRemove":
# Removed function, because with plugin paths + clean library, it will wipe
# entire library if user has permissions. Instead, use the emby context menu available
# in Isengard and higher version
pass pass
'''try:
kodiid = data['id']
type = data['type']
except (KeyError, TypeError):
log.info("Item is invalid for emby deletion.")
else:
# Send the delete action 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:
log.info("Could not find itemid in emby database.")
else:
if settings('skipContextMenu') != "true":
resp = xbmcgui.Dialog().yesno(
heading="Confirm delete",
line1="Delete file on Emby Server?")
if not resp:
log.info("User skipped deletion.")
embycursor.close()
return
url = "{server}/emby/Items/%s?format=json" % itemid
log.info("Deleting request: %s" % itemid)
doUtils.downloadUrl(url, action_type="DELETE")
finally:
embycursor.close()'''
elif method == "System.OnSleep": elif method == "System.OnSleep":
# Connection is going to sleep # Connection is going to sleep
@ -154,18 +121,15 @@ class KodiMonitor(xbmc.Monitor):
elif method == "System.OnWake": elif method == "System.OnWake":
# Allow network to wake up # Allow network to wake up
xbmc.sleep(10000) sleep(10000)
window('plex_onWake', value="true") window('plex_onWake', value="true")
window('plex_online', value="false") window('plex_online', value="false")
elif method == "GUI.OnScreensaverDeactivated": elif method == "GUI.OnScreensaverDeactivated":
if settings('dbSyncScreensaver') == "true": if settings('dbSyncScreensaver') == "true":
xbmc.sleep(5000) sleep(5000)
window('plex_runLibScan', value="full") window('plex_runLibScan', value="full")
elif method == "Playlist.OnClear":
pass
def PlayBackStart(self, data): def PlayBackStart(self, data):
""" """
Called whenever a playback is started Called whenever a playback is started
@ -177,7 +141,7 @@ class KodiMonitor(xbmc.Monitor):
currentFile = None currentFile = None
count = 0 count = 0
while currentFile is None: while currentFile is None:
xbmc.sleep(100) sleep(100)
try: try:
currentFile = self.xbmcplayer.getPlayingFile() currentFile = self.xbmcplayer.getPlayingFile()
except: except:
@ -201,7 +165,7 @@ class KodiMonitor(xbmc.Monitor):
# Try to get a Kodi ID # Try to get a Kodi ID
# If PKC was used - native paths, not direct paths # If PKC was used - native paths, not direct paths
plexid = window('emby_%s.itemid' % tryEncode(currentFile)) plexid = window('plex_%s.itemid' % tryEncode(currentFile))
# Get rid of the '' if the window property was not set # Get rid of the '' if the window property was not set
plexid = None if not plexid else plexid plexid = None if not plexid else plexid
kodiid = None kodiid = None
@ -215,27 +179,16 @@ class KodiMonitor(xbmc.Monitor):
# When using Widgets, Kodi doesn't tell us shit so we need this hack # When using Widgets, Kodi doesn't tell us shit so we need this hack
if (kodiid is None and plexid is None and typus != 'song' if (kodiid is None and plexid is None and typus != 'song'
and not currentFile.startswith('http')): and not currentFile.startswith('http')):
try: (kodiid, typus) = get_kodiid_from_filename(currentFile)
filename = currentFile.rsplit('/', 1)[1] if kodiid is None:
path = currentFile.rsplit('/', 1)[0] + '/' return
except IndexError:
filename = currentFile.rsplit('\\', 1)[1]
path = currentFile.rsplit('\\', 1)[0] + '\\'
log.debug('Trying to figure out playing item from filename: %s '
'and path: %s' % (filename, path))
with kodidb.GetKodiDB('video') as kodi_db:
try:
kodiid, typus = kodi_db.getIdFromFilename(filename, path)
except TypeError:
log.info('Abort playback report, could not id kodi item')
return
if plexid is None: if plexid is None:
# Get Plex' item id # Get Plex' item id
with embydb.GetEmbyDB() as emby_db: with plexdb.Get_Plex_DB() as plexcursor:
emby_dbitem = emby_db.getItem_byKodiId(kodiid, typus) plex_dbitem = plexcursor.getItem_byKodiId(kodiid, typus)
try: try:
plexid = emby_dbitem[0] plexid = plex_dbitem[0]
except TypeError: except TypeError:
log.info("No Plex id returned for kodiid %s. Aborting playback" log.info("No Plex id returned for kodiid %s. Aborting playback"
" report" % kodiid) " report" % kodiid)
@ -256,24 +209,25 @@ class KodiMonitor(xbmc.Monitor):
# Save currentFile for cleanup later and to be able to access refs # Save currentFile for cleanup later and to be able to access refs
window('plex_lastPlayedFiled', value=currentFile) window('plex_lastPlayedFiled', value=currentFile)
window('plex_currently_playing_itemid', value=plexid) window('plex_currently_playing_itemid', value=plexid)
window("emby_%s.itemid" % tryEncode(currentFile), value=plexid) window("plex_%s.itemid" % tryEncode(currentFile), value=plexid)
log.info('Finish playback startup') log.info('Finish playback startup')
def StartDirectPath(self, plexid, type, currentFile): def StartDirectPath(self, plexid, type, currentFile):
""" """
Set some additional stuff if playback was initiated by Kodi, not PKC Set some additional stuff if playback was initiated by Kodi, not PKC
""" """
result = self.doUtils('{server}/library/metadata/%s' % plexid) xml = self.doUtils('{server}/library/metadata/%s' % plexid)
try: try:
result[0].attrib xml[0].attrib
except: except:
log.error('Did not receive a valid XML for plexid %s.' % plexid) log.error('Did not receive a valid XML for plexid %s.' % plexid)
return False return False
# Setup stuff, because playback was started by Kodi, not PKC # Setup stuff, because playback was started by Kodi, not PKC
pbutils.PlaybackUtils(result[0]).setProperties( api = API(xml[0])
currentFile, xbmcgui.ListItem()) listitem = api.CreateListItemFromPlexItem()
api.set_playback_win_props(currentFile, listitem)
if type == "song" and settings('streamMusic') == "true": if type == "song" and settings('streamMusic') == "true":
window('emby_%s.playmethod' % currentFile, value="DirectStream") window('plex_%s.playmethod' % currentFile, value="DirectStream")
else: else:
window('emby_%s.playmethod' % currentFile, value="DirectPlay") window('plex_%s.playmethod' % currentFile, value="DirectPlay")
log.debug('Window properties set for direct paths!') log.debug('Window properties set for direct paths!')

View file

@ -11,7 +11,7 @@ import xbmc
import xbmcgui import xbmcgui
import xbmcvfs import xbmcvfs
from utils import window, settings, getUnixTimestamp, kodiSQL, sourcesXML,\ from utils import window, settings, getUnixTimestamp, sourcesXML,\
ThreadMethods, ThreadMethodsAdditionalStop, LogTime, getScreensaver,\ ThreadMethods, ThreadMethodsAdditionalStop, LogTime, getScreensaver,\
setScreensaver, playlistXSP, language as lang, DateToKodi, reset,\ setScreensaver, playlistXSP, language as lang, DateToKodi, reset,\
advancedSettingsXML, getKodiVideoDBPath, tryDecode, deletePlaylists,\ advancedSettingsXML, getKodiVideoDBPath, tryDecode, deletePlaylists,\
@ -19,7 +19,7 @@ from utils import window, settings, getUnixTimestamp, kodiSQL, sourcesXML,\
import clientinfo import clientinfo
import downloadutils import downloadutils
import itemtypes import itemtypes
import embydb_functions as embydb import plexdb_functions as plexdb
import kodidb_functions as kodidb import kodidb_functions as kodidb
import userclient import userclient
import videonodes import videonodes
@ -302,9 +302,9 @@ class ProcessFanartThread(Thread):
# Leave the Plex art untouched # Leave the Plex art untouched
allartworks = None allartworks = None
else: else:
with embydb.GetEmbyDB() as emby_db: with plexdb.Get_Plex_DB() as plex_db:
try: try:
kodiId = emby_db.getItem_byId(item['itemId'])[0] kodiId = plex_db.getItem_byId(item['itemId'])[0]
except TypeError: except TypeError:
log.error('Could not get Kodi id for plex id %s' log.error('Could not get Kodi id for plex id %s'
% item['itemId']) % item['itemId'])
@ -356,19 +356,10 @@ class ProcessFanartThread(Thread):
@ThreadMethods @ThreadMethods
class LibrarySync(Thread): class LibrarySync(Thread):
""" """
librarysync.LibrarySync(queue)
where (communication with websockets)
queue: Queue object for background sync
""" """
# Borg, even though it's planned to only have 1 instance up and running! def __init__(self, callback=None):
_shared_state = {} self.mgr = callback
def __init__(self, queue):
self.__dict__ = self._shared_state
# Communication with websockets
self.queue = queue
self.itemsToProcess = [] self.itemsToProcess = []
self.sessionKeys = [] self.sessionKeys = []
self.fanartqueue = Queue.Queue() self.fanartqueue = Queue.Queue()
@ -455,7 +446,9 @@ class LibrarySync(Thread):
return False return False
plexId = None plexId = None
for mediatype in ('movie', 'show', 'artist'): for mediatype in (PF.PLEX_TYPE_MOVIE,
PF.PLEX_TYPE_SHOW,
PF.PLEX_TYPE_ARTIST):
if plexId is not None: if plexId is not None:
break break
for view in sections: for view in sections:
@ -539,24 +532,34 @@ class LibrarySync(Thread):
def initializeDBs(self): def initializeDBs(self):
""" """
Run once during startup to verify that emby db exists. Run once during startup to verify that plex db exists.
""" """
embyconn = kodiSQL('emby') with plexdb.Get_Plex_DB() as plex_db:
embycursor = embyconn.cursor() # Create the tables for the plex database
# Create the tables for the emby database plex_db.plexcursor.execute('''
# emby, view, version CREATE TABLE IF NOT EXISTS plex(
embycursor.execute( plex_id TEXT UNIQUE,
"""CREATE TABLE IF NOT EXISTS emby( view_id TEXT,
emby_id TEXT UNIQUE, media_folder TEXT, emby_type TEXT, media_type TEXT, kodi_id INTEGER, plex_type TEXT,
kodi_fileid INTEGER, kodi_pathid INTEGER, parent_id INTEGER, checksum INTEGER)""") kodi_type TEXT,
embycursor.execute( kodi_id INTEGER,
"""CREATE TABLE IF NOT EXISTS view( kodi_fileid INTEGER,
view_id TEXT UNIQUE, view_name TEXT, media_type TEXT, kodi_tagid INTEGER)""") kodi_pathid INTEGER,
embycursor.execute("CREATE TABLE IF NOT EXISTS version(idVersion TEXT)") parent_id INTEGER,
embyconn.commit() checksum INTEGER)
''')
# content sync: movies, tvshows, musicvideos, music plex_db.plexcursor.execute('''
embyconn.close() CREATE TABLE IF NOT EXISTS view(
view_id TEXT UNIQUE,
view_name TEXT,
kodi_type TEXT,
kodi_tagid INTEGER)
''')
plex_db.plexcursor.execute('''
CREATE TABLE IF NOT EXISTS version(idVersion TEXT)
''')
# Create an index for actors to speed up sync
create_actor_db_index()
# Create an index for actors to speed up sync # Create an index for actors to speed up sync
create_actor_db_index() create_actor_db_index()
@ -643,12 +646,13 @@ class LibrarySync(Thread):
log.error('Path hack failed with error message: %s' % str(e)) log.error('Path hack failed with error message: %s' % str(e))
return True return True
def processView(self, folderItem, kodi_db, emby_db, totalnodes): def processView(self, folderItem, kodi_db, plex_db, totalnodes):
vnodes = self.vnodes vnodes = self.vnodes
folder = folderItem.attrib folder = folderItem.attrib
mediatype = folder['type'] mediatype = folder['type']
# Only process supported formats # Only process supported formats
if mediatype not in ('movie', 'show', 'artist', 'photo'): if mediatype not in (PF.PLEX_TYPE_MOVIE, PF.PLEX_TYPE_SHOW,
PF.PLEX_TYPE_ARTIST, PF.PLEX_TYPE_PHOTO):
return totalnodes return totalnodes
# Prevent duplicate for nodes of the same type # Prevent duplicate for nodes of the same type
@ -661,8 +665,8 @@ class LibrarySync(Thread):
foldername = folder['title'] foldername = folder['title']
viewtype = folder['type'] viewtype = folder['type']
# Get current media folders from emby database # Get current media folders from plex database
view = emby_db.getView_byId(folderid) view = plex_db.getView_byId(folderid)
try: try:
current_viewname = view[0] current_viewname = view[0]
current_viewtype = view[1] current_viewtype = view[1]
@ -672,12 +676,12 @@ class LibrarySync(Thread):
tagid = kodi_db.createTag(foldername) tagid = kodi_db.createTag(foldername)
# Create playlist for the video library # Create playlist for the video library
if (foldername not in playlists and if (foldername not in playlists and
mediatype in ('movie', 'show', 'musicvideos')): mediatype in (PF.PLEX_TYPE_MOVIE, PF.PLEX_TYPE_SHOW)):
playlistXSP(mediatype, foldername, folderid, viewtype) playlistXSP(mediatype, foldername, folderid, viewtype)
playlists.append(foldername) playlists.append(foldername)
# Create the video node # Create the video node
if (foldername not in nodes and if (foldername not in nodes and
mediatype not in ("musicvideos", "artist")): mediatype != PF.PLEX_TYPE_ARTIST):
vnodes.viewNode(sorted_views.index(foldername), vnodes.viewNode(sorted_views.index(foldername),
foldername, foldername,
mediatype, mediatype,
@ -685,8 +689,8 @@ class LibrarySync(Thread):
folderid) folderid)
nodes.append(foldername) nodes.append(foldername)
totalnodes += 1 totalnodes += 1
# Add view to emby database # Add view to plex database
emby_db.addView(folderid, foldername, viewtype, tagid) plex_db.addView(folderid, foldername, viewtype, tagid)
else: else:
log.info(' '.join(( log.info(' '.join((
"Found viewid: %s" % folderid, "Found viewid: %s" % folderid,
@ -708,10 +712,10 @@ class LibrarySync(Thread):
tagid = kodi_db.createTag(foldername) tagid = kodi_db.createTag(foldername)
# Update view with new info # Update view with new info
emby_db.updateView(foldername, tagid, folderid) plex_db.updateView(foldername, tagid, folderid)
if mediatype != "artist": if mediatype != "artist":
if emby_db.getView_byName(current_viewname) is None: if plex_db.getView_byName(current_viewname) is None:
# The tag could be a combined view. Ensure there's # The tag could be a combined view. Ensure there's
# no other tags with the same name before deleting # no other tags with the same name before deleting
# playlist. # playlist.
@ -731,7 +735,7 @@ class LibrarySync(Thread):
delete=True) delete=True)
# Added new playlist # Added new playlist
if (foldername not in playlists and if (foldername not in playlists and
mediatype in ('movie', 'show', 'musicvideos')): mediatype in (PF.PLEX_TYPE_MOVIE, PF.PLEX_TYPE_SHOW)):
playlistXSP(mediatype, playlistXSP(mediatype,
foldername, foldername,
folderid, folderid,
@ -748,16 +752,16 @@ class LibrarySync(Thread):
totalnodes += 1 totalnodes += 1
# Update items with new tag # Update items with new tag
items = emby_db.getItem_byView(folderid) items = plex_db.getItem_byView(folderid)
for item in items: for item in items:
# Remove the "s" from viewtype for tags # Remove the "s" from viewtype for tags
kodi_db.updateTag( kodi_db.updateTag(
current_tagid, tagid, item[0], current_viewtype[:-1]) current_tagid, tagid, item[0], current_viewtype[:-1])
else: else:
# Validate the playlist exists or recreate it # Validate the playlist exists or recreate it
if mediatype != "artist": if mediatype != PF.PLEX_TYPE_ARTIST:
if (foldername not in playlists and if (foldername not in playlists and
mediatype in ('movie', 'show', 'musicvideos')): mediatype in (PF.PLEX_TYPE_MOVIE, PF.PLEX_TYPE_SHOW)):
playlistXSP(mediatype, playlistXSP(mediatype,
foldername, foldername,
folderid, folderid,
@ -792,22 +796,22 @@ class LibrarySync(Thread):
# For whatever freaking reason, .copy() or dict() does NOT work?!?!?! # For whatever freaking reason, .copy() or dict() does NOT work?!?!?!
self.nodes = { self.nodes = {
'movie': [], PF.PLEX_TYPE_MOVIE: [],
'show': [], PF.PLEX_TYPE_SHOW: [],
'artist': [], PF.PLEX_TYPE_ARTIST: [],
'photo': [] PF.PLEX_TYPE_PHOTO: []
} }
self.playlists = { self.playlists = {
'movie': [], PF.PLEX_TYPE_MOVIE: [],
'show': [], PF.PLEX_TYPE_SHOW: [],
'artist': [], PF.PLEX_TYPE_ARTIST: [],
'photo': [] PF.PLEX_TYPE_PHOTO: []
} }
self.sorted_views = [] self.sorted_views = []
for view in sections: for view in sections:
itemType = view.attrib['type'] itemType = view.attrib['type']
if itemType in ('movie', 'show', 'photo'): # NOT artist for now if itemType in (PF.PLEX_TYPE_MOVIE, PF.PLEX_TYPE_SHOW, PF.PLEX_TYPE_PHOTO): # NOT artist for now
self.sorted_views.append(view.attrib['title']) self.sorted_views.append(view.attrib['title'])
log.debug('Sorted views: %s' % self.sorted_views) log.debug('Sorted views: %s' % self.sorted_views)
@ -815,15 +819,15 @@ class LibrarySync(Thread):
vnodes.clearProperties() vnodes.clearProperties()
totalnodes = len(self.sorted_views) totalnodes = len(self.sorted_views)
with embydb.GetEmbyDB() as emby_db: with plexdb.Get_Plex_DB() as plex_db:
# Backup old views to delete them later, if needed (at the end # Backup old views to delete them later, if needed (at the end
# of this method, only unused views will be left in oldviews) # of this method, only unused views will be left in oldviews)
self.old_views = emby_db.getViews() self.old_views = plex_db.getViews()
with kodidb.GetKodiDB('video') as kodi_db: with kodidb.GetKodiDB('video') as kodi_db:
for folderItem in sections: for folderItem in sections:
totalnodes = self.processView(folderItem, totalnodes = self.processView(folderItem,
kodi_db, kodi_db,
emby_db, plex_db,
totalnodes) totalnodes)
# Add video nodes listings # Add video nodes listings
# Plex: there seem to be no favorites/favorites tag # Plex: there seem to be no favorites/favorites tag
@ -842,19 +846,17 @@ class LibrarySync(Thread):
# "movies", # "movies",
# "channels") # "channels")
# totalnodes += 1 # totalnodes += 1
with kodidb.GetKodiDB('music') as kodi_db:
pass
# Save total # Save total
window('Plex.nodes.total', str(totalnodes)) window('Plex.nodes.total', str(totalnodes))
# Reopen DB connection to ensure that changes were commited before # Reopen DB connection to ensure that changes were commited before
with embydb.GetEmbyDB() as emby_db: with plexdb.Get_Plex_DB() as plex_db:
log.info("Removing views: %s" % self.old_views) log.info("Removing views: %s" % self.old_views)
for view in self.old_views: for view in self.old_views:
emby_db.removeView(view) plex_db.removeView(view)
# update views for all: # update views for all:
self.views = emby_db.getAllViewInfo() self.views = plex_db.getAllViewInfo()
log.info("Finished processing views. Views saved: %s" % self.views) log.info("Finished processing views. Views saved: %s" % self.views)
return True return True
@ -1038,9 +1040,10 @@ class LibrarySync(Thread):
if (settings('FanartTV') == 'true' and if (settings('FanartTV') == 'true' and
itemType in ('Movies', 'TVShows')): itemType in ('Movies', 'TVShows')):
# Save to queue for later processing # Save to queue for later processing
typus = {'Movies': 'movie', 'TVShows': 'tvshow'}[itemType] typus = {'Movies': PF.KODI_TYPE_MOVIE,
'TVShows': PF.KODI_TYPE_SHOW}[itemType]
for item in self.updatelist: for item in self.updatelist:
if item['mediaType'] in ('movie', 'show'): if item['mediaType'] in (PF.KODI_TYPE_MOVIE, PF.KODI_TYPE_SHOW):
self.fanartqueue.put({ self.fanartqueue.put({
'itemId': item['itemId'], 'itemId': item['itemId'],
'class': itemType, 'class': itemType,
@ -1056,16 +1059,17 @@ class LibrarySync(Thread):
itemType = 'Movies' itemType = 'Movies'
views = [x for x in self.views if x['itemtype'] == 'movie'] views = [x for x in self.views if x['itemtype'] == PF.KODI_TYPE_MOVIE]
log.info("Processing Plex %s. Libraries: %s" % (itemType, views)) log.info("Processing Plex %s. Libraries: %s" % (itemType, views))
self.allKodiElementsId = {} self.allKodiElementsId = {}
if self.compare: if self.compare:
with embydb.GetEmbyDB() as emby_db: with plexdb.Get_Plex_DB() as plex_db:
# Get movies from Plex server # Get movies from Plex server
# Pull the list of movies and boxsets in Kodi # Pull the list of movies and boxsets in Kodi
try: try:
self.allKodiElementsId = dict(emby_db.getChecksum('Movie')) self.allKodiElementsId = dict(
plex_db.getChecksum(PF.PLEX_TYPE_MOVIE))
except ValueError: except ValueError:
self.allKodiElementsId = {} self.allKodiElementsId = {}
@ -1148,11 +1152,13 @@ class LibrarySync(Thread):
self.allKodiElementsId = {} self.allKodiElementsId = {}
if self.compare: if self.compare:
with embydb.GetEmbyDB() as emby_db: with plexdb.Get_Plex_DB() as plex:
# Pull the list of TV shows already in Kodi # Pull the list of TV shows already in Kodi
for kind in ('Series', 'Season', 'Episode'): for kind in (PF.PLEX_TYPE_SHOW,
PF.PLEX_TYPE_SEASON,
PF.PLEX_TYPE_EPISODE):
try: try:
elements = dict(emby_db.getChecksum(kind)) elements = dict(plex.getChecksum(kind))
self.allKodiElementsId.update(elements) self.allKodiElementsId.update(elements)
# Yet empty/not yet synched # Yet empty/not yet synched
except ValueError: except ValueError:
@ -1270,22 +1276,24 @@ class LibrarySync(Thread):
def PlexMusic(self): def PlexMusic(self):
itemType = 'Music' itemType = 'Music'
views = [x for x in self.views if x['itemtype'] == 'artist'] views = [x for x in self.views if x['itemtype'] == PF.PLEX_TYPE_ARTIST]
log.info("Media folders for %s: %s" % (itemType, views)) log.info("Media folders for %s: %s" % (itemType, views))
methods = { methods = {
'MusicArtist': 'add_updateArtist', PF.PLEX_TYPE_ARTIST: 'add_updateArtist',
'MusicAlbum': 'add_updateAlbum', PF.PLEX_TYPE_ALBUM: 'add_updateAlbum',
'Audio': 'add_updateSong' PF.PLEX_TYPE_SONG: 'add_updateSong'
} }
urlArgs = { urlArgs = {
'MusicArtist': {'type': 8}, PF.PLEX_TYPE_ARTIST: {'type': 8},
'MusicAlbum': {'type': 9}, PF.PLEX_TYPE_ALBUM: {'type': 9},
'Audio': {'type': 10} PF.PLEX_TYPE_SONG: {'type': 10}
} }
# Process artist, then album and tracks last to minimize overhead # Process artist, then album and tracks last to minimize overhead
for kind in ('MusicArtist', 'MusicAlbum', 'Audio'): for kind in (PF.PLEX_TYPE_ARTIST,
PF.PLEX_TYPE_ALBUM,
PF.PLEX_TYPE_SONG):
if self.threadStopped(): if self.threadStopped():
return False return False
log.debug("Start processing music %s" % kind) log.debug("Start processing music %s" % kind)
@ -1318,10 +1326,10 @@ class LibrarySync(Thread):
# Get a list of items already existing in Kodi db # Get a list of items already existing in Kodi db
if self.compare: if self.compare:
with embydb.GetEmbyDB() as emby_db: with plexdb.Get_Plex_DB() as plex_db:
# Pull the list of items already in Kodi # Pull the list of items already in Kodi
try: try:
elements = dict(emby_db.getChecksum(kind)) elements = dict(plex_db.getChecksum(kind))
self.allKodiElementsId.update(elements) self.allKodiElementsId.update(elements)
# Yet empty/nothing yet synched # Yet empty/nothing yet synched
except ValueError: except ValueError:
@ -1569,14 +1577,14 @@ class LibrarySync(Thread):
where where
""" """
items = [] items = []
with embydb.GetEmbyDB() as emby_db: with plexdb.Get_Plex_DB() as plex_db:
for item in data: for item in data:
# Drop buffering messages immediately # Drop buffering messages immediately
state = item.get('state') state = item.get('state')
if state == 'buffering': if state == 'buffering':
continue continue
ratingKey = item.get('ratingKey') ratingKey = item.get('ratingKey')
kodiInfo = emby_db.getItem_byId(ratingKey) kodiInfo = plex_db.getItem_byId(ratingKey)
if kodiInfo is None: if kodiInfo is None:
# Item not (yet) in Kodi library # Item not (yet) in Kodi library
continue continue
@ -1663,7 +1671,7 @@ class LibrarySync(Thread):
# Now tell Kodi where we are # Now tell Kodi where we are
for item in items: for item in items:
itemFkt = getattr(itemtypes, itemFkt = getattr(itemtypes,
PF.ITEMTYPE_FROM_PLEXTYPE[item['kodi_type']]) PF.ITEMTYPE_FROM_KODITYPE[item['kodi_type']])
with itemFkt() as Fkt: with itemFkt() as Fkt:
Fkt.updatePlaystate(item) Fkt.updatePlaystate(item)
@ -1675,12 +1683,12 @@ class LibrarySync(Thread):
""" """
items = [] items = []
typus = { typus = {
'Movie': 'Movies', PF.PLEX_TYPE_MOVIE: 'Movies',
'Series': 'TVShows' PF.PLEX_TYPE_SHOW: 'TVShows'
} }
with embydb.GetEmbyDB() as emby_db: with plexdb.Get_Plex_DB() as plex_db:
for plextype in typus: for plextype in typus:
items.extend(emby_db.itemsByType(plextype)) items.extend(plex_db.itemsByType(plextype))
# Shuffle the list to not always start out identically # Shuffle the list to not always start out identically
shuffle(items) shuffle(items)
for item in items: for item in items:
@ -1720,7 +1728,8 @@ class LibrarySync(Thread):
xbmcplayer = xbmc.Player() xbmcplayer = xbmc.Player()
queue = self.queue # Link to Websocket queue
queue = self.mgr.ws.queue
startupComplete = False startupComplete = False
self.views = [] self.views = []

View file

@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
###############################################################################
import logging
from threading import Thread
from Queue import Queue
from xbmc import sleep
from utils import window, ThreadMethods
###############################################################################
log = logging.getLogger("PLEX."+__name__)
###############################################################################
@ThreadMethods
class Monitor_Kodi_Play(Thread):
"""
Monitors for new plays initiated on the Kodi side with addon paths.
Immediately throws them into a queue to be processed by playback_starter
"""
# Borg - multiple instances, shared state
def __init__(self, callback=None):
self.mgr = callback
self.playback_queue = Queue()
Thread.__init__(self)
def run(self):
threadStopped = self.threadStopped
queue = self.playback_queue
log.info("----===## Starting Kodi_Play_Client ##===----")
while not threadStopped():
if window('plex_play_new_item'):
queue.put(window('plex_play_new_item'))
window('plex_play_new_item', clear=True)
else:
sleep(20)
# Put one last item into the queue to let playback_starter end
queue.put(None)
log.info("----===## Kodi_Play_Client stopped ##===----")

44
resources/lib/pickler.py Normal file
View file

@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
###############################################################################
import logging
import cPickle as Pickle
from utils import pickl_window
###############################################################################
log = logging.getLogger("PLEX."+__name__)
###############################################################################
def pickle_me(obj, window_var='plex_result'):
"""
Pickles the obj to the window variable. Use to transfer Python
objects between different PKC python instances (e.g. if default.py is
called and you'd want to use the service.py instance)
obj can be pretty much any Python object. However, classes and
functions won't work. See the Pickle documentation
"""
log.debug('Start pickling: %s' % obj)
pickl_window(window_var, value=Pickle.dumps(obj))
log.debug('Successfully pickled')
def unpickle_me(window_var='plex_result'):
"""
Unpickles a Python object from the window variable window_var.
Will then clear the window variable!
"""
result = pickl_window(window_var)
pickl_window(window_var, clear=True)
log.debug('Start unpickling')
obj = Pickle.loads(result)
log.debug('Successfully unpickled: %s' % obj)
return obj
class Playback_Successful(object):
"""
Used to communicate with another PKC Python instance
"""
listitem = None

View file

@ -0,0 +1,100 @@
# -*- coding: utf-8 -*-
###############################################################################
import logging
from threading import Thread
from urlparse import parse_qsl
from PKC_listitem import PKC_ListItem
from pickler import pickle_me, Playback_Successful
from playbackutils import PlaybackUtils
from utils import window
from PlexFunctions import GetPlexMetadata, PLEX_TYPE_PHOTO, \
KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE
from PlexAPI import API
from playqueue import lock
###############################################################################
log = logging.getLogger("PLEX."+__name__)
###############################################################################
class Playback_Starter(Thread):
"""
Processes new plays
"""
def __init__(self, callback=None):
self.mgr = callback
self.playqueue = self.mgr.playqueue
Thread.__init__(self)
def process_play(self, plex_id, kodi_id=None):
"""
Processes Kodi playback init for ONE item
"""
log.info("Process_play called with plex_id %s, kodi_id %s"
% (plex_id, kodi_id))
if window('plex_authenticated') != "true":
log.error('Not yet authenticated for PMS, abort starting playback')
# Todo: Warn user with dialog
return
xml = GetPlexMetadata(plex_id)
try:
xml[0].attrib
except (TypeError, AttributeError):
log.error('Could not get a PMS xml for plex id %s' % plex_id)
return
api = API(xml[0])
if api.getType() == PLEX_TYPE_PHOTO:
# Photo
result = Playback_Successful()
listitem = PKC_ListItem()
listitem = api.CreateListItemFromPlexItem(listitem)
api.AddStreamInfo(listitem)
api.set_listitem_artwork(listitem)
result.listitem = listitem
else:
# Video and Music
playqueue = self.playqueue.get_playqueue_from_type(
KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.getType()])
with lock:
result = PlaybackUtils(xml, playqueue).play(
plex_id,
kodi_id,
xml.attrib.get('librarySectionUUID'))
log.info('Done process_play, playqueues: %s'
% self.playqueue.playqueues)
return result
def triage(self, item):
mode, params = item.split('?', 1)
params = dict(parse_qsl(params))
log.debug('Received mode: %s, params: %s' % (mode, params))
try:
if mode == 'play':
result = self.process_play(params.get('id'),
params.get('dbid'))
elif mode == 'companion':
result = self.process_companion()
except:
log.error('Error encountered for mode %s, params %s'
% (mode, params))
import traceback
log.error(traceback.format_exc())
# Let default.py know!
pickle_me(None)
else:
pickle_me(result)
def run(self):
queue = self.mgr.monitor_kodi_play.playback_queue
log.info("----===## Starting Playback_Starter ##===----")
while True:
item = queue.get()
if item is None:
# Need to shutdown - initiated by monitor_kodi_play
break
else:
self.triage(item)
queue.task_done()
log.info("----===## Playback_Starter stopped ##===----")

View file

@ -3,20 +3,25 @@
############################################################################### ###############################################################################
import logging import logging
import sys
from urllib import urlencode from urllib import urlencode
from threading import Thread
import xbmc from xbmc import getCondVisibility, Player
import xbmcgui import xbmcgui
import xbmcplugin
import playutils as putils import playutils as putils
import playlist
from utils import window, settings, tryEncode, tryDecode from utils import window, settings, tryEncode, tryDecode
import downloadutils import downloadutils
import PlexAPI from PlexAPI import API
import PlexFunctions as PF from PlexFunctions import GetPlexPlaylist, KODITYPE_FROM_PLEXTYPE, \
PLEX_TYPE_CLIP, PLEX_TYPE_MOVIE
from PKC_listitem import PKC_ListItem as ListItem
from playlist_func import add_item_to_kodi_playlist, \
get_playlist_details_from_xml, add_listitem_to_Kodi_playlist, \
add_listitem_to_playlist, remove_from_Kodi_playlist
from pickler import Playback_Successful
from plexdb_functions import Get_Plex_DB
############################################################################### ###############################################################################
@ -29,74 +34,72 @@ addonName = "PlexKodiConnect"
class PlaybackUtils(): class PlaybackUtils():
def __init__(self, item): def __init__(self, xml, playqueue):
self.xml = xml
self.playqueue = playqueue
self.item = item def play(self, plex_id, kodi_id=None, plex_lib_UUID=None):
self.API = PlexAPI.API(item) """
plex_lib_UUID: xml attribute 'librarySectionUUID', needed for posting
self.userid = window('currUserId') to the PMS
self.server = window('pms_server') """
log.info("Playbackutils called")
if self.API.getType() == 'track': item = self.xml[0]
self.pl = playlist.Playlist(typus='music') api = API(item)
else: playqueue = self.playqueue
self.pl = playlist.Playlist(typus='video') xml = None
result = Playback_Successful()
def play(self, itemid, dbid=None): listitem = ListItem()
playutils = putils.PlayUtils(item)
item = self.item
# Hack to get only existing entry in PMS response for THIS instance of
# playbackutils :-)
self.API = PlexAPI.API(item[0])
API = self.API
listitem = xbmcgui.ListItem()
playutils = putils.PlayUtils(item[0])
log.info("Play called.")
playurl = playutils.getPlayUrl() playurl = playutils.getPlayUrl()
if not playurl: if not playurl:
return xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, listitem) log.error('No playurl found, aborting')
return
if dbid in (None, 'plextrailer', 'plexnode'): if kodi_id in (None, 'plextrailer', 'plexnode'):
# Item is not in Kodi database, is a trailer or plex redirect # Item is not in Kodi database, is a trailer/clip or plex redirect
# e.g. plex.tv watch later # e.g. plex.tv watch later
API.CreateListItemFromPlexItem(listitem) api.CreateListItemFromPlexItem(listitem)
self.setArtwork(listitem) api.set_listitem_artwork(listitem)
if dbid == 'plexnode': if kodi_id == 'plexnode':
# Need to get yet another xml to get final url # Need to get yet another xml to get final url
window('emby_%s.playmethod' % playurl, clear=True) window('plex_%s.playmethod' % playurl, clear=True)
xml = downloadutils.DownloadUtils().downloadUrl( xml = downloadutils.DownloadUtils().downloadUrl(
'{server}%s' % item[0][0][0].attrib.get('key')) '{server}%s' % item[0][0].attrib.get('key'))
if xml in (None, 401): try:
xml[0].attrib
except (TypeError, AttributeError):
log.error('Could not download %s' log.error('Could not download %s'
% item[0][0][0].attrib.get('key')) % item[0][0].attrib.get('key'))
return xbmcplugin.setResolvedUrl( return
int(sys.argv[1]), False, listitem)
playurl = tryEncode(xml[0].attrib.get('key')) playurl = tryEncode(xml[0].attrib.get('key'))
window('emby_%s.playmethod' % playurl, value='DirectStream') window('plex_%s.playmethod' % playurl, value='DirectStream')
playmethod = window('emby_%s.playmethod' % playurl) playmethod = window('plex_%s.playmethod' % playurl)
if playmethod == "Transcode": if playmethod == "Transcode":
window('emby_%s.playmethod' % playurl, clear=True) window('plex_%s.playmethod' % playurl, clear=True)
playurl = tryEncode(playutils.audioSubsPref( playurl = tryEncode(playutils.audioSubsPref(
listitem, tryDecode(playurl))) listitem, tryDecode(playurl)))
window('emby_%s.playmethod' % playurl, "Transcode") window('plex_%s.playmethod' % playurl, "Transcode")
listitem.setPath(playurl) listitem.setPath(playurl)
self.setProperties(playurl, listitem) api.set_playback_win_props(playurl, listitem)
return xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem) result.listitem = listitem
return result
############### ORGANIZE CURRENT PLAYLIST ################ kodi_type = KODITYPE_FROM_PLEXTYPE[api.getType()]
kodi_id = int(kodi_id)
# ORGANIZE CURRENT PLAYLIST ################
contextmenu_play = window('plex_contextplay') == 'true' contextmenu_play = window('plex_contextplay') == 'true'
window('plex_contextplay', clear=True) window('plex_contextplay', clear=True)
homeScreen = xbmc.getCondVisibility('Window.IsActive(home)') homeScreen = getCondVisibility('Window.IsActive(home)')
kodiPl = self.pl.playlist sizePlaylist = len(playqueue.items)
sizePlaylist = kodiPl.size()
if contextmenu_play: if contextmenu_play:
# Need to start with the items we're inserting here # Need to start with the items we're inserting here
startPos = sizePlaylist startPos = sizePlaylist
else: else:
# Can return -1 # Can return -1
startPos = max(kodiPl.getposition(), 0) startPos = max(playqueue.kodi_pl.getposition(), 0)
self.currentPosition = startPos self.currentPosition = startPos
propertiesPlayback = window('plex_playbackProps') == "true" propertiesPlayback = window('plex_playbackProps') == "true"
@ -108,72 +111,106 @@ class PlaybackUtils():
log.info("Playlist plugin position: %s" % self.currentPosition) log.info("Playlist plugin position: %s" % self.currentPosition)
log.info("Playlist size: %s" % sizePlaylist) log.info("Playlist size: %s" % sizePlaylist)
############### RESUME POINT ################ # RESUME POINT ################
seektime, runtime = API.getRuntime() seektime, runtime = api.getRuntime()
if window('plex_customplaylist.seektime'):
# Already got seektime, e.g. from playqueue & Plex companion
seektime = int(window('plex_customplaylist.seektime'))
# We need to ensure we add the intro and additional parts only once. # We need to ensure we add the intro and additional parts only once.
# Otherwise we get a loop. # Otherwise we get a loop.
if not propertiesPlayback: if not propertiesPlayback:
window('plex_playbackProps', value="true") window('plex_playbackProps', value="true")
log.info("Setting up properties in playlist.") log.info("Setting up properties in playlist.")
# Where will the player need to start?
# Do we need to get trailers?
trailers = False
if (api.getType() == PLEX_TYPE_MOVIE and
not seektime and
sizePlaylist < 2 and
settings('enableCinema') == "true"):
if settings('askCinema') == "true":
trailers = xbmcgui.Dialog().yesno(
addonName,
"Play trailers?")
else:
trailers = True
# Post to the PMS. REUSE THE PLAYQUEUE!
xml = GetPlexPlaylist(
plex_id,
plex_lib_UUID,
mediatype=api.getType(),
trailers=trailers)
get_playlist_details_from_xml(playqueue, xml=xml)
if (not homeScreen and not seektime and if (not homeScreen and not seektime and sizePlaylist < 2 and
window('plex_customplaylist') != "true" and window('plex_customplaylist') != "true" and
not contextmenu_play): not contextmenu_play):
# Need to add a dummy file because the first item will fail
log.debug("Adding dummy file to playlist.") log.debug("Adding dummy file to playlist.")
dummyPlaylist = True dummyPlaylist = True
kodiPl.add(playurl, listitem, index=startPos) add_listitem_to_Kodi_playlist(
playqueue,
startPos,
xbmcgui.ListItem(),
playurl,
xml[0])
# Remove the original item from playlist # Remove the original item from playlist
self.pl.removefromPlaylist(startPos+1) remove_from_Kodi_playlist(
# Readd the original item to playlist - via jsonrpc so we have full metadata playqueue,
self.pl.insertintoPlaylist( startPos+1)
# Readd the original item to playlist - via jsonrpc so we have
# full metadata
add_item_to_kodi_playlist(
playqueue,
self.currentPosition+1, self.currentPosition+1,
dbid, kodi_id=kodi_id,
PF.KODITYPE_FROM_PLEXTYPE[API.getType()]) kodi_type=kodi_type,
file=playurl)
self.currentPosition += 1 self.currentPosition += 1
############### -- CHECK FOR INTROS ################ # -- ADD TRAILERS ################
if (settings('enableCinema') == "true" and not seektime): if trailers:
# if we have any play them when the movie/show is not being resumed for i, item in enumerate(xml):
xml = PF.GetPlexPlaylist( if i == len(xml) - 1:
itemid, # Don't add the main movie itself
item.attrib.get('librarySectionUUID'), break
mediatype=API.getType()) self.add_trailer(item)
introsPlaylist = self.AddTrailers(xml) introsPlaylist = True
############### -- ADD MAIN ITEM ONLY FOR HOMESCREEN ##############
# -- ADD MAIN ITEM ONLY FOR HOMESCREEN ##############
if homeScreen and not seektime and not sizePlaylist: if homeScreen and not seektime and not sizePlaylist:
# Extend our current playlist with the actual item to play # Extend our current playlist with the actual item to play
# only if there's no playlist first # only if there's no playlist first
log.info("Adding main item to playlist.") log.info("Adding main item to playlist.")
self.pl.addtoPlaylist( add_item_to_kodi_playlist(
dbid, playqueue,
PF.KODITYPE_FROM_PLEXTYPE[API.getType()]) self.currentPosition,
kodi_id,
kodi_type)
elif contextmenu_play: elif contextmenu_play:
if window('useDirectPaths') == 'true': if window('useDirectPaths') == 'true':
# Cannot add via JSON with full metadata because then we # Cannot add via JSON with full metadata because then we
# Would be using the direct path # Would be using the direct path
log.debug("Adding contextmenu item for direct paths") log.debug("Adding contextmenu item for direct paths")
if window('emby_%s.playmethod' % playurl) == "Transcode": if window('plex_%s.playmethod' % playurl) == "Transcode":
window('emby_%s.playmethod' % playurl, window('plex_%s.playmethod' % playurl,
clear=True) clear=True)
playurl = tryEncode(playutils.audioSubsPref( playurl = tryEncode(playutils.audioSubsPref(
listitem, tryDecode(playurl))) listitem, tryDecode(playurl)))
window('emby_%s.playmethod' % playurl, window('plex_%s.playmethod' % playurl,
value="Transcode") value="Transcode")
self.setProperties(playurl, listitem) api.CreateListItemFromPlexItem(listitem)
self.setArtwork(listitem) api.set_playback_win_props(playurl, listitem)
API.CreateListItemFromPlexItem(listitem) api.set_listitem_artwork(listitem)
kodiPl.add(playurl, listitem, index=self.currentPosition+1) kodiPl.add(playurl, listitem, index=self.currentPosition+1)
else: else:
# Full metadata # Full metadata
self.pl.insertintoPlaylist( self.pl.insertintoPlaylist(
self.currentPosition+1, self.currentPosition+1,
dbid, kodi_id,
PF.KODITYPE_FROM_PLEXTYPE[API.getType()]) kodi_type)
self.currentPosition += 1 self.currentPosition += 1
if seektime: if seektime:
window('plex_customplaylist.seektime', value=str(seektime)) window('plex_customplaylist.seektime', value=str(seektime))
@ -181,177 +218,145 @@ class PlaybackUtils():
# Ensure that additional parts are played after the main item # Ensure that additional parts are played after the main item
self.currentPosition += 1 self.currentPosition += 1
############### -- CHECK FOR ADDITIONAL PARTS ################ # -- CHECK FOR ADDITIONAL PARTS ################
if len(item[0][0]) > 1: if len(item[0]) > 1:
# Only add to the playlist after intros have played self.add_part(item, api, kodi_id, kodi_type)
for counter, part in enumerate(item[0][0]):
# Never add first part
if counter == 0:
continue
# Set listitem and properties for each additional parts
API.setPartNumber(counter)
additionalListItem = xbmcgui.ListItem()
additionalPlayurl = playutils.getPlayUrl(
partNumber=counter)
log.debug("Adding additional part: %s, url: %s"
% (counter, additionalPlayurl))
self.setProperties(additionalPlayurl, additionalListItem)
self.setArtwork(additionalListItem)
# NEW to Plex
API.CreateListItemFromPlexItem(additionalListItem)
kodiPl.add(additionalPlayurl, additionalListItem,
index=self.currentPosition)
self.pl.verifyPlaylist()
self.currentPosition += 1
API.setPartNumber(0)
if dummyPlaylist: if dummyPlaylist:
# Added a dummy file to the playlist, # Added a dummy file to the playlist,
# because the first item is going to fail automatically. # because the first item is going to fail automatically.
log.info("Processed as a playlist. First item is skipped.") log.info("Processed as a playlist. First item is skipped.")
return xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, listitem) # Delete the item that's gonna fail!
del playqueue.items[startPos]
# Don't attach listitem
return result
# We just skipped adding properties. Reset flag for next time. # We just skipped adding properties. Reset flag for next time.
elif propertiesPlayback: elif propertiesPlayback:
log.debug("Resetting properties playback flag.") log.debug("Resetting properties playback flag.")
window('plex_playbackProps', clear=True) window('plex_playbackProps', clear=True)
#self.pl.verifyPlaylist() # SETUP MAIN ITEM ##########
########## SETUP MAIN ITEM ##########
# For transcoding only, ask for audio/subs pref # For transcoding only, ask for audio/subs pref
if (window('emby_%s.playmethod' % playurl) == "Transcode" and if (window('plex_%s.playmethod' % playurl) == "Transcode" and
not contextmenu_play): not contextmenu_play):
window('emby_%s.playmethod' % playurl, clear=True) window('plex_%s.playmethod' % playurl, clear=True)
playurl = tryEncode(playutils.audioSubsPref( playurl = tryEncode(playutils.audioSubsPref(
listitem, tryDecode(playurl))) listitem, tryDecode(playurl)))
window('emby_%s.playmethod' % playurl, value="Transcode") window('plex_%s.playmethod' % playurl, value="Transcode")
listitem.setPath(playurl) listitem.setPath(playurl)
self.setProperties(playurl, listitem) api.set_playback_win_props(playurl, listitem)
api.set_listitem_artwork(listitem)
############### PLAYBACK ################ # PLAYBACK ################
if (homeScreen and seektime and window('plex_customplaylist') != "true" if (homeScreen and seektime and window('plex_customplaylist') != "true"
and not contextmenu_play): and not contextmenu_play):
log.info("Play as a widget item.") log.info("Play as a widget item")
API.CreateListItemFromPlexItem(listitem) api.CreateListItemFromPlexItem(listitem)
xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem) result.listitem = listitem
return result
elif ((introsPlaylist and window('plex_customplaylist') == "true") or elif ((introsPlaylist and window('plex_customplaylist') == "true") or
(homeScreen and not sizePlaylist) or (homeScreen and not sizePlaylist) or
contextmenu_play): contextmenu_play):
# Playlist was created just now, play it. # Playlist was created just now, play it.
# Contextmenu plays always need this # Contextmenu plays always need this
log.info("Play playlist.") log.info("Play playlist from starting position %s" % startPos)
xbmcplugin.endOfDirectory(int(sys.argv[1]), True, False, False) # Need a separate thread because Player won't return in time
xbmc.Player().play(kodiPl, startpos=startPos) thread = Thread(target=Player().play,
args=(playqueue.kodi_pl, None, False, startPos))
thread.setDaemon(True)
thread.start()
# Don't attach listitem
return result
else: else:
log.info("Play as a regular item.") log.info("Play as a regular item")
xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem) result.listitem = listitem
return result
def AddTrailers(self, xml): def play_all(self):
""" """
Adds trailers to a movie, if applicable. Returns True if trailers were Play all items contained in the xml passed in. Called by Plex Companion
added
""" """
# Failure when downloading trailer playQueue log.info("Playbackutils play_all called")
if xml in (None, 401): window('plex_playbackProps', value="true")
return False self.currentPosition = 0
# Failure when getting trailers, e.g. when no plex pass for item in self.xml:
if xml.attrib.get('size') == '1': api = API(item)
return False if api.getType() == PLEX_TYPE_CLIP:
self.add_trailer(item)
if settings('askCinema') == "true": continue
resp = xbmcgui.Dialog().yesno(addonName, "Play trailers?") with Get_Plex_DB() as plex_db:
if not resp: db_item = plex_db.getItem_byId(api.getRatingKey())
# User selected to not play trailers try:
log.info("Skip trailers.") add_item_to_kodi_playlist(self.playqueue,
return False self.currentPosition,
kodi_id=db_item[0],
kodi_type=db_item[4])
self.currentPosition += 1
if len(item[0]) > 1:
self.add_part(item,
api,
db_item[0],
db_item[4])
except TypeError:
# Item not in Kodi DB
self.add_trailer(item)
continue
def add_trailer(self, item):
# Playurl needs to point back so we can get metadata! # Playurl needs to point back so we can get metadata!
path = "plugin://plugin.video.plexkodiconnect.movies/" path = "plugin://plugin.video.plexkodiconnect/movies/"
params = { params = {
'mode': "play", 'mode': "play",
'dbid': 'plextrailer' 'dbid': 'plextrailer'
} }
for counter, intro in enumerate(xml): introAPI = API(item)
# Don't process the last item - it's the original movie listitem = introAPI.CreateListItemFromPlexItem()
if counter == len(xml)-1: params['id'] = introAPI.getRatingKey()
break params['filename'] = introAPI.getKey()
# The server randomly returns intros, process them. introPlayurl = path + '?' + urlencode(params)
# introListItem = xbmcgui.ListItem() introAPI.set_listitem_artwork(listitem)
# introPlayurl = putils.PlayUtils(intro).getPlayUrl() # Overwrite the Plex url
introAPI = PlexAPI.API(intro) listitem.setPath(introPlayurl)
params['id'] = introAPI.getRatingKey() log.info("Adding Plex trailer: %s" % introPlayurl)
params['filename'] = introAPI.getKey() add_listitem_to_Kodi_playlist(
introPlayurl = path + '?' + urlencode(params) self.playqueue,
log.info("Adding Intro: %s" % introPlayurl) self.currentPosition,
listitem,
introPlayurl,
xml_video_element=item)
self.currentPosition += 1
self.pl.insertintoPlaylist(self.currentPosition, url=introPlayurl) def add_part(self, item, api, kodi_id, kodi_type):
"""
Adds an additional part to the playlist
"""
# Only add to the playlist after intros have played
for counter, part in enumerate(item[0]):
# Never add first part
if counter == 0:
continue
# Set listitem and properties for each additional parts
api.setPartNumber(counter)
additionalListItem = xbmcgui.ListItem()
playutils = putils.PlayUtils(item)
additionalPlayurl = playutils.getPlayUrl(
partNumber=counter)
log.debug("Adding additional part: %s, url: %s"
% (counter, additionalPlayurl))
api.CreateListItemFromPlexItem(additionalListItem)
api.set_playback_win_props(additionalPlayurl,
additionalListItem)
api.set_listitem_artwork(additionalListItem)
add_listitem_to_playlist(
self.playqueue,
self.currentPosition,
additionalListItem,
kodi_id=kodi_id,
kodi_type=kodi_type,
plex_id=api.getRatingKey(),
file=additionalPlayurl)
self.currentPosition += 1 self.currentPosition += 1
api.setPartNumber(0)
return True
def setProperties(self, playurl, listitem):
# Set all properties necessary for plugin path playback
itemid = self.API.getRatingKey()
itemtype = self.API.getType()
userdata = self.API.getUserData()
embyitem = "emby_%s" % playurl
window('%s.runtime' % embyitem, value=str(userdata['Runtime']))
window('%s.type' % embyitem, value=itemtype)
window('%s.itemid' % embyitem, value=itemid)
window('%s.playcount' % embyitem, value=str(userdata['PlayCount']))
if itemtype == "episode":
window('%s.refreshid' % embyitem,
value=self.API.getParentRatingKey())
else:
window('%s.refreshid' % embyitem, value=itemid)
# Append external subtitles to stream
playmethod = window('%s.playmethod' % embyitem)
if playmethod in ("DirectStream", "DirectPlay"):
subtitles = self.API.externalSubs(playurl)
listitem.setSubtitles(subtitles)
self.setArtwork(listitem)
def setArtwork(self, listItem):
allartwork = self.API.getAllArtwork(parentInfo=True)
arttypes = {
'poster': "Primary",
'tvshow.poster': "Thumb",
'clearart': "Art",
'tvshow.clearart': "Art",
'clearart': "Primary",
'tvshow.clearart': "Primary",
'clearlogo': "Logo",
'tvshow.clearlogo': "Logo",
'discart': "Disc",
'fanart_image': "Backdrop",
'landscape': "Backdrop",
"banner": "Banner"
}
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})

View file

@ -11,8 +11,9 @@ from utils import window, settings, language as lang, DateToKodi, \
getUnixTimestamp getUnixTimestamp
import clientinfo import clientinfo
import downloadutils import downloadutils
import embydb_functions as embydb import plexdb_functions as plexdb
import kodidb_functions as kodidb import kodidb_functions as kodidb
from PlexFunctions import KODI_TYPE_MOVIE, KODI_TYPE_EPISODE
############################################################################### ###############################################################################
@ -76,11 +77,11 @@ class Player(xbmc.Player):
self.currentFile = currentFile self.currentFile = currentFile
window('plex_lastPlayedFiled', value=currentFile) window('plex_lastPlayedFiled', value=currentFile)
# We may need to wait for info to be set in kodi monitor # We may need to wait for info to be set in kodi monitor
itemId = window("emby_%s.itemid" % currentFile) itemId = window("plex_%s.itemid" % currentFile)
count = 0 count = 0
while not itemId: while not itemId:
xbmc.sleep(200) xbmc.sleep(200)
itemId = window("emby_%s.itemid" % currentFile) itemId = window("plex_%s.itemid" % currentFile)
if count == 5: if count == 5:
log.warn("Could not find itemId, cancelling playback report!") log.warn("Could not find itemId, cancelling playback report!")
return return
@ -88,16 +89,16 @@ class Player(xbmc.Player):
log.info("ONPLAYBACK_STARTED: %s itemid: %s" % (currentFile, itemId)) log.info("ONPLAYBACK_STARTED: %s itemid: %s" % (currentFile, itemId))
embyitem = "emby_%s" % currentFile plexitem = "plex_%s" % currentFile
runtime = window("%s.runtime" % embyitem) runtime = window("%s.runtime" % plexitem)
refresh_id = window("%s.refreshid" % embyitem) refresh_id = window("%s.refreshid" % plexitem)
playMethod = window("%s.playmethod" % embyitem) playMethod = window("%s.playmethod" % plexitem)
itemType = window("%s.type" % embyitem) itemType = window("%s.type" % plexitem)
try: try:
playcount = int(window("%s.playcount" % embyitem)) playcount = int(window("%s.playcount" % plexitem))
except ValueError: except ValueError:
playcount = 0 playcount = 0
window('emby_skipWatched%s' % itemId, value="true") window('plex_skipWatched%s' % itemId, value="true")
log.debug("Playing itemtype is: %s" % itemType) log.debug("Playing itemtype is: %s" % itemType)
@ -134,7 +135,7 @@ class Player(xbmc.Player):
volume = result.get('volume') volume = result.get('volume')
muted = result.get('muted') muted = result.get('muted')
# Postdata structure to send to Emby server # Postdata structure to send to plex server
url = "{server}/:/timeline?" url = "{server}/:/timeline?"
postdata = { postdata = {
@ -154,7 +155,7 @@ class Player(xbmc.Player):
postdata['AudioStreamIndex'] = window("%sAudioStreamIndex" % currentFile) postdata['AudioStreamIndex'] = window("%sAudioStreamIndex" % currentFile)
postdata['SubtitleStreamIndex'] = window("%sSubtitleStreamIndex" % currentFile) postdata['SubtitleStreamIndex'] = window("%sSubtitleStreamIndex" % currentFile)
else: else:
# Get the current kodi audio and subtitles and convert to Emby equivalent # Get the current kodi audio and subtitles and convert to plex equivalent
tracks_query = { tracks_query = {
"jsonrpc": "2.0", "jsonrpc": "2.0",
"id": 1, "id": 1,
@ -190,9 +191,9 @@ class Player(xbmc.Player):
# Postdata for the subtitles # Postdata for the subtitles
if subsEnabled and len(xbmc.Player().getAvailableSubtitleStreams()) > 0: if subsEnabled and len(xbmc.Player().getAvailableSubtitleStreams()) > 0:
# Number of audiotracks to help get Emby Index # Number of audiotracks to help get plex Index
audioTracks = len(xbmc.Player().getAvailableAudioStreams()) audioTracks = len(xbmc.Player().getAvailableAudioStreams())
mapping = window("%s.indexMapping" % embyitem) mapping = window("%s.indexMapping" % plexitem)
if mapping: # Set in playbackutils.py if mapping: # Set in playbackutils.py
@ -229,10 +230,10 @@ class Player(xbmc.Player):
log.error('Could not get kodi runtime, setting to zero') log.error('Could not get kodi runtime, setting to zero')
runtime = 0 runtime = 0
with embydb.GetEmbyDB() as emby_db: with plexdb.Get_Plex_DB() as plex_db:
emby_dbitem = emby_db.getItem_byId(itemId) plex_dbitem = plex_db.getItem_byId(itemId)
try: try:
fileid = emby_dbitem[1] fileid = plex_dbitem[1]
except TypeError: except TypeError:
log.info("Could not find fileid in plex db.") log.info("Could not find fileid in plex db.")
fileid = None fileid = None
@ -338,7 +339,7 @@ class Player(xbmc.Player):
playMethod = data['playmethod'] playMethod = data['playmethod']
# Prevent manually mark as watched in Kodi monitor # Prevent manually mark as watched in Kodi monitor
window('emby_skipWatched%s' % itemid, value="true") window('plex_skipWatched%s' % itemid, value="true")
if currentPosition and runtime: if currentPosition and runtime:
try: try:
@ -353,7 +354,7 @@ class Player(xbmc.Player):
if percentComplete >= markPlayed: if percentComplete >= markPlayed:
# Tell Kodi that we've finished watching (Plex knows) # Tell Kodi that we've finished watching (Plex knows)
if (data['fileid'] is not None and if (data['fileid'] is not None and
data['itemType'] in ('movie', 'episode')): data['itemType'] in (KODI_TYPE_MOVIE, KODI_TYPE_EPISODE)):
with kodidb.GetKodiDB('video') as kodi_db: with kodidb.GetKodiDB('video') as kodi_db:
kodi_db.addPlaystate( kodi_db.addPlaystate(
data['fileid'], data['fileid'],
@ -391,13 +392,13 @@ class Player(xbmc.Player):
# Clean the WINDOW properties # Clean the WINDOW properties
for filename in self.played_info: for filename in self.played_info:
cleanup = ( cleanup = (
'emby_%s.itemid' % filename, 'plex_%s.itemid' % filename,
'emby_%s.runtime' % filename, 'plex_%s.runtime' % filename,
'emby_%s.refreshid' % filename, 'plex_%s.refreshid' % filename,
'emby_%s.playmethod' % filename, 'plex_%s.playmethod' % filename,
'emby_%s.type' % filename, 'plex_%s.type' % filename,
'emby_%s.runtime' % filename, 'plex_%s.runtime' % filename,
'emby_%s.playcount' % filename, 'plex_%s.playcount' % filename,
'plex_%s.playlistPosition' % filename 'plex_%s.playlistPosition' % filename
) )
for item in cleanup: for item in cleanup:

View file

@ -1,337 +0,0 @@
# -*- coding: utf-8 -*-
###############################################################################
import logging
import json
from urllib import urlencode
from threading import Lock
from functools import wraps
import xbmc
import embydb_functions as embydb
from utils import window, tryEncode
import playbackutils
import PlexFunctions
import PlexAPI
###############################################################################
log = logging.getLogger("PLEX."+__name__)
###############################################################################
class lockMethod:
"""
Decorator for class methods to lock hem completely. Same lock is used for
every single decorator and instance used!
Here only used for Playlist()
"""
lock = Lock()
@classmethod
def decorate(cls, func):
@wraps(func)
def wrapper(*args, **kwargs):
with cls.lock:
result = func(*args, **kwargs)
return result
return wrapper
class Playlist():
"""
Initiate with Playlist(typus='video' or 'music')
"""
# Borg - multiple instances, shared state
_shared_state = {}
typus = None
queueId = None
playQueueVersion = None
guid = None
playlistId = None
player = xbmc.Player()
# "interal" PKC playlist
items = []
@lockMethod.decorate
def __init__(self, typus=None):
# Borg
self.__dict__ = self._shared_state
self.userid = window('currUserId')
self.server = window('pms_server')
# Construct the Kodi playlist instance
if self.typus == typus:
return
if typus == 'video':
self.playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
self.typus = 'video'
log.info('Initiated video playlist')
elif typus == 'music':
self.playlist = xbmc.PlayList(xbmc.PLAYLIST_MUSIC)
self.typus = 'music'
log.info('Initiated music playlist')
else:
self.playlist = None
self.typus = None
log.info('Empty playlist initiated')
if self.playlist is not None:
self.playlistId = self.playlist.getPlayListId()
@lockMethod.decorate
def getQueueIdFromPosition(self, playlistPosition):
return self.items[playlistPosition]['playQueueItemID']
@lockMethod.decorate
def Typus(self, value=None):
if value:
self.typus = value
else:
return self.typus
@lockMethod.decorate
def PlayQueueVersion(self, value=None):
if value:
self.playQueueVersion = value
else:
return self.playQueueVersion
@lockMethod.decorate
def QueueId(self, value=None):
if value:
self.queueId = value
else:
return self.queueId
@lockMethod.decorate
def Guid(self, value=None):
if value:
self.guid = value
else:
return self.guid
@lockMethod.decorate
def clear(self):
"""
Empties current Kodi playlist and associated variables
"""
log.info('Clearing playlist')
self.playlist.clear()
self.items = []
self.queueId = None
self.playQueueVersion = None
self.guid = None
def _initiatePlaylist(self):
log.info('Initiating playlist')
playlist = None
with embydb.GetEmbyDB() as emby_db:
for item in self.items:
itemid = item['plexId']
embydb_item = emby_db.getItem_byId(itemid)
try:
mediatype = embydb_item[4]
except TypeError:
log.info('Couldnt find item %s in Kodi db' % itemid)
item = PlexFunctions.GetPlexMetadata(itemid)
if item in (None, 401):
log.info('Couldnt find item %s on PMS, trying next'
% itemid)
continue
if PlexAPI.API(item[0]).getType() == 'track':
playlist = xbmc.PlayList(xbmc.PLAYLIST_MUSIC)
log.info('Music playlist initiated')
self.typus = 'music'
else:
playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
log.info('Video playlist initiated')
self.typus = 'video'
else:
if mediatype == 'song':
playlist = xbmc.PlayList(xbmc.PLAYLIST_MUSIC)
log.info('Music playlist initiated')
self.typus = 'music'
else:
playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
log.info('Video playlist initiated')
self.typus = 'video'
break
self.playlist = playlist
if self.playlist is not None:
self.playlistId = self.playlist.getPlayListId()
def _processItems(self, startitem, startPlayer=False):
startpos = None
with embydb.GetEmbyDB() as emby_db:
for pos, item in enumerate(self.items):
kodiId = None
plexId = item['plexId']
embydb_item = emby_db.getItem_byId(plexId)
try:
kodiId = embydb_item[0]
mediatype = embydb_item[4]
except TypeError:
log.info('Couldnt find item %s in Kodi db' % plexId)
xml = PlexFunctions.GetPlexMetadata(plexId)
if xml in (None, 401):
log.error('Could not download plexId %s' % plexId)
else:
log.debug('Downloaded xml metadata, adding now')
self._addtoPlaylist_xbmc(xml[0])
else:
# Add to playlist
log.debug("Adding %s PlexId %s, KodiId %s to playlist."
% (mediatype, plexId, kodiId))
self._addtoPlaylist(kodiId, mediatype)
# Add the kodiId
if kodiId is not None:
item['kodiId'] = str(kodiId)
if (startpos is None and startitem[1] == item[startitem[0]]):
startpos = pos
if startPlayer is True and len(self.playlist) > 0:
if startpos is not None:
self.player.play(self.playlist, startpos=startpos)
else:
log.info('Never received a starting item for playlist, '
'starting with the first entry')
self.player.play(self.playlist)
@lockMethod.decorate
def playAll(self, items, startitem, offset):
"""
items: list of dicts of the form
{
'playQueueItemID': Plex playQueueItemID, e.g. '29175'
'plexId': Plex ratingKey, e.g. '125'
'kodiId': Kodi's db id of the same item
}
startitem: tuple (typus, id), where typus is either
'playQueueItemID' or 'plexId' and id is the corresponding
id as a string
offset: First item's time offset to play in Kodi time (an int)
"""
log.info("---*** PLAY ALL ***---")
log.debug('Startitem: %s, offset: %s, items: %s'
% (startitem, offset, items))
self.items = items
if self.playlist is None:
self._initiatePlaylist()
if self.playlist is None:
log.error('Could not create playlist, abort')
return
window('plex_customplaylist', value="true")
if offset != 0:
# Seek to the starting position
window('plex_customplaylist.seektime', str(offset))
self._processItems(startitem, startPlayer=True)
# Log playlist
self._verifyPlaylist()
log.debug('Internal playlist: %s' % self.items)
@lockMethod.decorate
def modifyPlaylist(self, itemids):
log.info("---*** MODIFY PLAYLIST ***---")
log.debug("Items: %s" % itemids)
self._initiatePlaylist(itemids)
self._processItems(itemids, startPlayer=True)
self._verifyPlaylist()
@lockMethod.decorate
def addtoPlaylist(self, dbid=None, mediatype=None, url=None):
"""
mediatype: Kodi type: 'movie', 'episode', 'musicvideo', 'artist',
'album', 'song', 'genre'
"""
self._addtoPlaylist(dbid, mediatype, url)
def _addtoPlaylist(self, dbid=None, mediatype=None, url=None):
pl = {
'jsonrpc': "2.0",
'id': 1,
'method': "Playlist.Add",
'params': {
'playlistid': self.playlistId
}
}
if dbid is not None:
pl['params']['item'] = {'%sid' % tryEncode(mediatype): int(dbid)}
else:
pl['params']['item'] = {'file': url}
log.debug(xbmc.executeJSONRPC(json.dumps(pl)))
def _addtoPlaylist_xbmc(self, item):
API = PlexAPI.API(item)
params = {
'mode': "play",
'dbid': 'plextrailer',
'id': API.getRatingKey(),
'filename': API.getKey()
}
playurl = "plugin://plugin.video.plexkodiconnect.movies/?%s" \
% urlencode(params)
listitem = API.CreateListItemFromPlexItem()
playbackutils.PlaybackUtils(item).setArtwork(listitem)
self.playlist.add(playurl, listitem)
@lockMethod.decorate
def insertintoPlaylist(self,
position,
dbid=None,
mediatype=None,
url=None):
pl = {
'jsonrpc': "2.0",
'id': 1,
'method': "Playlist.Insert",
'params': {
'playlistid': self.playlistId,
'position': position
}
}
if dbid is not None:
pl['params']['item'] = {'%sid' % tryEncode(mediatype): int(dbid)}
else:
pl['params']['item'] = {'file': url}
log.debug(xbmc.executeJSONRPC(json.dumps(pl)))
@lockMethod.decorate
def verifyPlaylist(self):
self._verifyPlaylist()
def _verifyPlaylist(self):
pl = {
'jsonrpc': "2.0",
'id': 1,
'method': "Playlist.GetItems",
'params': {
'playlistid': self.playlistId,
'properties': ['title', 'file']
}
}
log.debug(xbmc.executeJSONRPC(json.dumps(pl)))
@lockMethod.decorate
def removefromPlaylist(self, position):
pl = {
'jsonrpc': "2.0",
'id': 1,
'method': "Playlist.Remove",
'params': {
'playlistid': self.playlistId,
'position': position
}
}
log.debug(xbmc.executeJSONRPC(json.dumps(pl)))

View file

@ -0,0 +1,519 @@
import logging
from urllib import quote
import plexdb_functions as plexdb
from downloadutils import DownloadUtils as DU
from utils import JSONRPC, tryEncode
from PlexAPI import API
###############################################################################
log = logging.getLogger("PLEX."+__name__)
###############################################################################
# kodi_item dict:
# {u'type': u'movie', u'id': 3, 'file': path-to-file}
class Playlist_Object_Baseclase(object):
playlistid = None # Kodi playlist ID, [int]
type = None # Kodi type: 'audio', 'video', 'picture'
kodi_pl = None # Kodi xbmc.PlayList object
items = [] # list of PLAYLIST_ITEMS
old_kodi_pl = [] # to store old Kodi JSON result with all pl items
ID = None # Plex id, e.g. playQueueID
version = None # Plex version, [int]
selectedItemID = None
selectedItemOffset = None
shuffled = 0 # [int], 0: not shuffled, 1: ??? 2: ???
repeat = 0 # [int], 0: not repeated, 1: ??? 2: ???
def __repr__(self):
answ = "<%s: " % (self.__class__.__name__)
# For some reason, can't use dir directly
answ += "ID: %s, " % self.ID
answ += "items: %s, " % self.items
for key in self.__dict__:
if key not in ("ID", 'items'):
answ += '%s: %s, ' % (key, getattr(self, key))
return answ[:-2] + ">"
def clear(self):
"""
Resets the playlist object to an empty playlist
"""
# Clear Kodi playlist object
self.kodi_pl.clear()
self.items = []
self.old_kodi_pl = []
self.ID = None
self.version = None
self.selectedItemID = None
self.selectedItemOffset = None
self.shuffled = 0
self.repeat = 0
log.debug('Playlist cleared: %s' % self)
def log_Kodi_playlist(self):
log.debug('Current Kodi playlist: %s' % get_kodi_playlist_items(self))
class Playlist_Object(Playlist_Object_Baseclase):
kind = 'playList'
class Playqueue_Object(Playlist_Object_Baseclase):
kind = 'playQueue'
class Playlist_Item(object):
ID = None # Plex playlist/playqueue id, e.g. playQueueItemID
plex_id = None # Plex unique item id, "ratingKey"
plex_UUID = None # Plex librarySectionUUID
kodi_id = None # Kodi unique kodi id (unique only within type!)
kodi_type = None # Kodi type: 'movie'
file = None # Path to the item's file
uri = None # Weird Plex uri path involving plex_UUID
guid = None # Weird Plex guid
def __repr__(self):
answ = "<%s: " % (self.__class__.__name__)
for key in self.__dict__:
answ += '%s: %s, ' % (key, getattr(self, key))
return answ[:-2] + ">"
def playlist_item_from_kodi(kodi_item):
"""
Turns the JSON answer from Kodi into a playlist element
Supply with data['item'] as returned from Kodi JSON-RPC interface.
kodi_item dict contains keys 'id', 'type', 'file' (if applicable)
"""
item = Playlist_Item()
item.kodi_id = kodi_item.get('id')
if item.kodi_id:
with plexdb.Get_Plex_DB() as plex_db:
plex_dbitem = plex_db.getItem_byKodiId(kodi_item['id'],
kodi_item['type'])
try:
item.plex_id = plex_dbitem[0]
item.plex_UUID = plex_dbitem[0] # we dont need the uuid yet :-)
except TypeError:
pass
item.file = kodi_item.get('file')
item.kodi_type = kodi_item.get('type')
if item.plex_id is None:
item.uri = 'library://whatever/item/%s' % quote(item.file, safe='')
else:
# TO BE VERIFIED - PLEX DOESN'T LIKE PLAYLIST ADDS IN THIS MANNER
item.uri = ('library://%s/item/library%%2Fmetadata%%2F%s' %
(item.plex_UUID, item.plex_id))
return item
def playlist_item_from_plex(plex_id):
"""
Returns a playlist element providing the plex_id ("ratingKey")
Returns a Playlist_Item
"""
item = Playlist_Item()
item.plex_id = plex_id
with plexdb.Get_Plex_DB() as plex_db:
plex_dbitem = plex_db.getItem_byId(plex_id)
try:
item.kodi_id = plex_dbitem[0]
item.kodi_type = plex_dbitem[4]
except:
raise KeyError('Could not find plex_id %s in database' % plex_id)
return item
def playlist_item_from_xml(playlist, xml_video_element):
"""
Returns a playlist element for the playqueue using the Plex xml
"""
item = Playlist_Item()
api = API(xml_video_element)
item.plex_id = api.getRatingKey()
item.ID = xml_video_element.attrib['%sItemID' % playlist.kind]
item.guid = xml_video_element.attrib.get('guid')
if item.plex_id:
with plexdb.Get_Plex_DB() as plex_db:
db_element = plex_db.getItem_byId(item.plex_id)
try:
item.kodi_id, item.kodi_type = int(db_element[0]), db_element[4]
except TypeError:
pass
log.debug('Created new playlist item from xml: %s' % item)
return item
def _log_xml(xml):
try:
xml.attrib
except AttributeError:
log.error('Did not receive an XML. Answer was: %s' % xml)
else:
from xml.etree.ElementTree import dump
log.error('XML received from the PMS:')
dump(xml)
def _get_playListVersion_from_xml(playlist, xml):
"""
Takes a PMS xml as input to overwrite the playlist version (e.g. Plex
playQueueVersion). Returns True if successful, False otherwise
"""
try:
playlist.version = int(xml.attrib['%sVersion' % playlist.kind])
except (TypeError, AttributeError, KeyError):
log.error('Could not get new playlist Version for playlist %s'
% playlist)
_log_xml(xml)
return False
return True
def get_playlist_details_from_xml(playlist, xml):
"""
Takes a PMS xml as input and overwrites all the playlist's details, e.g.
playlist.ID with the XML's playQueueID
"""
try:
playlist.ID = xml.attrib['%sID' % playlist.kind]
playlist.version = xml.attrib['%sVersion' % playlist.kind]
playlist.shuffled = xml.attrib['%sShuffled' % playlist.kind]
playlist.selectedItemID = xml.attrib.get(
'%sSelectedItemID' % playlist.kind)
playlist.selectedItemOffset = xml.attrib.get(
'%sSelectedItemOffset' % playlist.kind)
except:
log.error('Could not parse xml answer from PMS for playlist %s'
% playlist)
import traceback
log.error(traceback.format_exc())
_log_xml(xml)
raise KeyError
log.debug('Updated playlist from xml: %s' % playlist)
def update_playlist_from_PMS(playlist, playlist_id=None, xml=None):
"""
Updates Kodi playlist using a new PMS playlist. Pass in playlist_id if we
need to fetch a new playqueue
If an xml is passed in, the playlist will be overwritten with its info
"""
if xml is None:
xml = get_PMS_playlist(playlist, playlist_id)
try:
xml.attrib['%sVersion' % playlist.kind]
except:
log.error('Could not process Plex playlist')
return
# Clear our existing playlist and the associated Kodi playlist
playlist.clear()
# Set new values
get_playlist_details_from_xml(playlist, xml)
for plex_item in xml:
playlist.items.append(add_to_Kodi_playlist(playlist, plex_item))
def init_Plex_playlist(playlist, plex_id=None, kodi_item=None):
"""
Initializes the Plex side without changing the Kodi playlists
WILL ALSO UPDATE OUR PLAYLISTS
"""
log.debug('Initializing the playlist %s on the Plex side' % playlist)
if plex_id:
item = playlist_item_from_plex(plex_id)
else:
item = playlist_item_from_kodi(kodi_item)
params = {
'next': 0,
'type': playlist.type,
'uri': item.uri
}
xml = DU().downloadUrl(url="{server}/%ss" % playlist.kind,
action_type="POST",
parameters=params)
get_playlist_details_from_xml(playlist, xml)
playlist.items.append(item)
log.debug('Initialized the playlist on the Plex side: %s' % playlist)
def add_listitem_to_playlist(playlist, pos, listitem, kodi_id=None,
kodi_type=None, plex_id=None, file=None):
"""
Adds a listitem to both the Kodi and Plex playlist at position pos [int].
If file is not None, file will overrule kodi_id!
"""
log.debug('add_listitem_to_playlist. Playlist before add: %s' % playlist)
kodi_item = {'id': kodi_id, 'type': kodi_type, 'file': file}
if playlist.ID is None:
init_Plex_playlist(playlist, plex_id, kodi_item)
else:
add_item_to_PMS_playlist(playlist, pos, plex_id, kodi_item)
if kodi_id is None and playlist.items[pos].kodi_id:
kodi_id = playlist.items[pos].kodi_id
kodi_type = playlist.items[pos].kodi_type
if file is None:
file = playlist.items[pos].file
# Otherwise we double the item!
del playlist.items[pos]
kodi_item = {'id': kodi_id, 'type': kodi_type, 'file': file}
add_listitem_to_Kodi_playlist(playlist,
pos,
listitem,
file,
kodi_item=kodi_item)
def add_item_to_playlist(playlist, pos, kodi_id=None, kodi_type=None,
plex_id=None, file=None):
"""
Adds an item to BOTH the Kodi and Plex playlist at position pos [int]
"""
log.debug('add_item_to_playlist. Playlist before adding: %s' % playlist)
kodi_item = {'id': kodi_id, 'type': kodi_type, 'file': file}
if playlist.ID is None:
init_Plex_playlist(playlist, plex_id, kodi_item)
else:
add_item_to_PMS_playlist(playlist, pos, plex_id, kodi_item)
kodi_id = playlist.items[pos].kodi_id
kodi_type = playlist.items[pos].kodi_type
file = playlist.items[pos].file
add_item_to_kodi_playlist(playlist, pos, kodi_id, kodi_type, file)
def add_item_to_PMS_playlist(playlist, pos, plex_id=None, kodi_item=None):
"""
Adds a new item to the playlist at position pos [int] only on the Plex
side of things (e.g. because the user changed the Kodi side)
WILL ALSO UPDATE OUR PLAYLISTS
"""
log.debug('Adding new item plex_id: %s, kodi_item: %s on the Plex side at '
'position %s for %s' % (plex_id, kodi_item, pos, playlist))
if plex_id:
item = playlist_item_from_plex(plex_id)
else:
item = playlist_item_from_kodi(kodi_item)
url = "{server}/%ss/%s?uri=%s" % (playlist.kind, playlist.ID, item.uri)
# Will always put the new item at the end of the Plex playlist
xml = DU().downloadUrl(url, action_type="PUT")
try:
item.ID = xml[-1].attrib['%sItemID' % playlist.kind]
except IndexError:
log.info('Could not get playlist children. Adding a dummy')
except (TypeError, AttributeError, KeyError):
log.error('Could not add item %s to playlist %s'
% (kodi_item, playlist))
_log_xml(xml)
return
# Get the guid for this item
for plex_item in xml:
if plex_item.attrib['%sItemID' % playlist.kind] == item.ID:
item.guid = plex_item.attrib['guid']
playlist.items.append(item)
if pos == len(playlist.items) - 1:
# Item was added at the end
_get_playListVersion_from_xml(playlist, xml)
else:
# Move the new item to the correct position
move_playlist_item(playlist,
len(playlist.items) - 1,
pos)
log.debug('Successfully added item on the Plex side: %s' % playlist)
def add_item_to_kodi_playlist(playlist, pos, kodi_id=None, kodi_type=None,
file=None):
"""
Adds an item to the KODI playlist only
WILL ALSO UPDATE OUR PLAYLISTS
"""
log.debug('Adding new item kodi_id: %s, kodi_type: %s, file: %s to Kodi '
'only at position %s for %s'
% (kodi_id, kodi_type, file, pos, playlist))
params = {
'playlistid': playlist.playlistid,
'position': pos
}
if kodi_id is not None:
params['item'] = {'%sid' % kodi_type: int(kodi_id)}
else:
params['item'] = {'file': file}
log.debug(JSONRPC('Playlist.Insert').execute(params))
playlist.items.insert(pos, playlist_item_from_kodi(
{'id': kodi_id, 'type': kodi_type, 'file': file}))
def move_playlist_item(playlist, before_pos, after_pos):
"""
Moves playlist item from before_pos [int] to after_pos [int] for Plex only.
WILL ALSO CHANGE OUR PLAYLISTS
"""
log.debug('Moving item from %s to %s on the Plex side for %s'
% (before_pos, after_pos, playlist))
if after_pos == 0:
url = "{server}/%ss/%s/items/%s/move?after=0" % \
(playlist.kind,
playlist.ID,
playlist.items[before_pos].ID)
else:
url = "{server}/%ss/%s/items/%s/move?after=%s" % \
(playlist.kind,
playlist.ID,
playlist.items[before_pos].ID,
playlist.items[after_pos - 1].ID)
xml = DU().downloadUrl(url, action_type="PUT")
# We need to increment the playlistVersion
_get_playListVersion_from_xml(playlist, xml)
# Move our item's position in our internal playlist
playlist.items.insert(after_pos, playlist.items.pop(before_pos))
log.debug('Done moving for %s' % playlist)
def get_PMS_playlist(playlist, playlist_id=None):
"""
Fetches the PMS playlist/playqueue as an XML. Pass in playlist_id if we
need to fetch a new playlist
Returns None if something went wrong
"""
playlist_id = playlist_id if playlist_id else playlist.ID
xml = DU().downloadUrl(
"{server}/%ss/%s" % (playlist.kind, playlist_id),
headerOptions={'Accept': 'application/xml'})
try:
xml.attrib['%sID' % playlist.kind]
except (AttributeError, KeyError):
xml = None
return xml
def refresh_playlist_from_PMS(playlist):
"""
Only updates the selected item from the PMS side (e.g.
playQueueSelectedItemID). Will NOT check whether items still make sense.
"""
xml = get_PMS_playlist(playlist)
try:
xml.attrib['%sVersion' % playlist.kind]
except:
log.error('Could not download Plex playlist.')
return
get_playlist_details_from_xml(playlist, xml)
def delete_playlist_item_from_PMS(playlist, pos):
"""
Delete the item at position pos [int] on the Plex side and our playlists
"""
log.debug('Deleting position %s for %s on the Plex side' % (pos, playlist))
xml = DU().downloadUrl("{server}/%ss/%s/items/%s?repeat=%s" %
(playlist.kind,
playlist.ID,
playlist.items[pos].ID,
playlist.repeat),
action_type="DELETE")
_get_playListVersion_from_xml(playlist, xml)
del playlist.items[pos]
def get_kodi_playlist_items(playlist):
"""
Returns a list of the current Kodi playlist items using JSON
E.g.:
[{u'title': u'3 Idiots', u'type': u'movie', u'id': 3, u'file':
u'smb://nas/PlexMovies/3 Idiots 2009 pt1.mkv', u'label': u'3 Idiots'}]
"""
answ = JSONRPC('Playlist.GetItems').execute({
'playlistid': playlist.playlistid,
'properties': ["title", "file"]
})
try:
answ = answ['result']['items']
except KeyError:
answ = []
return answ
def get_kodi_playqueues():
"""
Example return: [{u'playlistid': 0, u'type': u'audio'},
{u'playlistid': 1, u'type': u'video'},
{u'playlistid': 2, u'type': u'picture'}]
"""
queues = JSONRPC('Playlist.GetPlaylists').execute()
try:
queues = queues['result']
except KeyError:
raise KeyError('Could not get Kodi playqueues. JSON Result was: %s'
% queues)
return queues
# Functions operating on the Kodi playlist objects ##########
def add_to_Kodi_playlist(playlist, xml_video_element):
"""
Adds a new item to the Kodi playlist via JSON (at the end of the playlist).
Pass in the PMS xml's video element (one level underneath MediaContainer).
Returns a Playlist_Item
"""
item = playlist_item_from_xml(playlist, xml_video_element)
params = {
'playlistid': playlist.playlistid
}
if item.kodi_id:
params['item'] = {'%sid' % item.kodi_type: item.kodi_id}
else:
params['item'] = {'file': tryEncode(item.file)}
log.debug(JSONRPC('Playlist.Add').execute(params))
return item
def add_listitem_to_Kodi_playlist(playlist, pos, listitem, file,
xml_video_element=None, kodi_item=None):
"""
Adds an xbmc listitem to the Kodi playlist.xml_video_element
WILL NOT UPDATE THE PLEX SIDE, BUT WILL UPDATE OUR PLAYLISTS
"""
log.debug('Insert listitem at position %s for Kodi only for %s'
% (pos, playlist))
# Add the item into Kodi playlist
playlist.kodi_pl.add(file, listitem, index=pos)
# We need to add this to our internal queue as well
if xml_video_element is not None:
item = playlist_item_from_xml(playlist, xml_video_element)
item.file = file
else:
item = playlist_item_from_kodi(kodi_item)
playlist.items.insert(pos, item)
log.debug('Done inserting for %s' % playlist)
def remove_from_Kodi_playlist(playlist, pos):
"""
Removes the item at position pos from the Kodi playlist using JSON.
WILL NOT UPDATE THE PLEX SIDE, BUT WILL UPDATE OUR PLAYLISTS
"""
log.debug('Removing position %s from Kodi only from %s' % (pos, playlist))
log.debug(JSONRPC('Playlist.Remove').execute({
'playlistid': playlist.playlistid,
'position': pos
}))
del playlist.items[pos]

185
resources/lib/playqueue.py Normal file
View file

@ -0,0 +1,185 @@
# -*- coding: utf-8 -*-
###############################################################################
import logging
from threading import RLock, Thread
from xbmc import sleep, Player, PlayList, PLAYLIST_MUSIC, PLAYLIST_VIDEO
from utils import window, ThreadMethods, ThreadMethodsAdditionalSuspend
import playlist_func as PL
from PlexFunctions import ConvertPlexToKodiTime
from playbackutils import PlaybackUtils
###############################################################################
log = logging.getLogger("PLEX."+__name__)
# Lock used for playqueue manipulations
lock = RLock()
###############################################################################
@ThreadMethodsAdditionalSuspend('plex_serverStatus')
@ThreadMethods
class Playqueue(Thread):
"""
Monitors Kodi's playqueues for changes on the Kodi side
"""
# Borg - multiple instances, shared state
__shared_state = {}
playqueues = None
def __init__(self, callback=None):
self.__dict__ = self.__shared_state
if self.playqueues is not None:
return
self.mgr = callback
# Initialize Kodi playqueues
with lock:
self.playqueues = []
for queue in PL.get_kodi_playqueues():
playqueue = PL.Playqueue_Object()
playqueue.playlistid = queue['playlistid']
playqueue.type = queue['type']
# Initialize each Kodi playlist
if playqueue.type == 'audio':
playqueue.kodi_pl = PlayList(PLAYLIST_MUSIC)
elif playqueue.type == 'video':
playqueue.kodi_pl = PlayList(PLAYLIST_VIDEO)
else:
# Currently, only video or audio playqueues available
playqueue.kodi_pl = PlayList(PLAYLIST_VIDEO)
self.playqueues.append(playqueue)
# sort the list by their playlistid, just in case
self.playqueues = sorted(
self.playqueues, key=lambda i: i.playlistid)
log.debug('Initialized the Kodi play queues: %s' % self.playqueues)
Thread.__init__(self)
def get_playqueue_from_type(self, typus):
"""
Returns the playqueue according to the typus ('video', 'audio',
'picture') passed in
"""
with lock:
for playqueue in self.playqueues:
if playqueue.type == typus:
break
else:
raise ValueError('Wrong playlist type passed in: %s' % typus)
return playqueue
def update_playqueue_from_PMS(self,
playqueue,
playqueue_id=None,
repeat=None,
offset=None):
"""
Completely updates the Kodi playqueue with the new Plex playqueue. Pass
in playqueue_id if we need to fetch a new playqueue
repeat = 0, 1, 2
offset = time offset in Plextime (milliseconds)
"""
log.info('New playqueue %s received from Plex companion with offset '
'%s, repeat %s' % (playqueue_id, offset, repeat))
with lock:
xml = PL.get_PMS_playlist(playqueue, playqueue_id)
if xml is None:
log.error('Could not get playqueue ID %s' % playqueue_id)
return
playqueue.clear()
PL.get_playlist_details_from_xml(playqueue, xml)
PlaybackUtils(xml, playqueue).play_all()
playqueue.repeat = 0 if not repeat else int(repeat)
window('plex_customplaylist', value="true")
if offset not in (None, "0"):
window('plex_customplaylist.seektime',
str(ConvertPlexToKodiTime(offset)))
for startpos, item in enumerate(playqueue.items):
if item.ID == playqueue.selectedItemID:
break
else:
startpos = 0
# Start playback. Player does not return in time
log.debug('Playqueues after Plex Companion update are now: %s'
% self.playqueues)
thread = Thread(target=Player().play,
args=(playqueue.kodi_pl,
None,
False,
startpos))
thread.setDaemon(True)
thread.start()
def _compare_playqueues(self, playqueue, new):
"""
Used to poll the Kodi playqueue and update the Plex playqueue if needed
"""
old = list(playqueue.items)
index = list(range(0, len(old)))
log.debug('Comparing new Kodi playqueue %s with our play queue %s'
% (new, old))
for i, new_item in enumerate(new):
for j, old_item in enumerate(old):
if self.threadStopped():
# Chances are that we got an empty Kodi playlist due to
# Kodi exit
return
if new_item.get('id') is None:
identical = old_item.file == new_item['file']
else:
identical = (old_item.kodi_id == new_item['id'] and
old_item.kodi_type == new_item['type'])
if j == 0 and identical:
del old[j], index[j]
break
elif identical:
log.debug('Detected playqueue item %s moved to position %s'
% (i+j, i))
PL.move_playlist_item(playqueue, i + j, i)
del old[j], index[j]
break
else:
log.debug('Detected new Kodi element at position %s: %s '
% (i, new_item))
if playqueue.ID is None:
PL.init_Plex_playlist(playqueue,
kodi_item=new_item)
else:
PL.add_item_to_PMS_playlist(playqueue,
i,
kodi_item=new_item)
index.insert(i, i)
for j in range(i+1, len(index)):
index[j] += 1
for i in reversed(index):
log.debug('Detected deletion of playqueue element at pos %s' % i)
PL.delete_playlist_item_from_PMS(playqueue, i)
log.debug('Done comparing playqueues')
def run(self):
threadStopped = self.threadStopped
threadSuspended = self.threadSuspended
log.info("----===## Starting PlayQueue client ##===----")
# Initialize the playqueues, if Kodi already got items in them
for playqueue in self.playqueues:
for i, item in enumerate(PL.get_kodi_playlist_items(playqueue)):
if i == 0:
PL.init_Plex_playlist(playqueue, kodi_item=item)
else:
PL.add_item_to_PMS_playlist(playqueue, i, kodi_item=item)
while not threadStopped():
while threadSuspended():
if threadStopped():
break
sleep(1000)
with lock:
for playqueue in self.playqueues:
kodi_playqueue = PL.get_kodi_playlist_items(playqueue)
if playqueue.old_kodi_pl != kodi_playqueue:
# compare old and new playqueue
self._compare_playqueues(playqueue, kodi_playqueue)
playqueue.old_kodi_pl = list(kodi_playqueue)
sleep(50)
log.info("----===## PlayQueue client stopped ##===----")

View file

@ -45,14 +45,14 @@ class PlayUtils():
log.info("File is direct playing.") log.info("File is direct playing.")
playurl = tryEncode(playurl) playurl = tryEncode(playurl)
# Set playmethod property # Set playmethod property
window('emby_%s.playmethod' % playurl, "DirectPlay") window('plex_%s.playmethod' % playurl, "DirectPlay")
elif self.isDirectStream(): elif self.isDirectStream():
log.info("File is direct streaming.") log.info("File is direct streaming.")
playurl = tryEncode( playurl = tryEncode(
self.API.getTranscodeVideoPath('DirectStream')) self.API.getTranscodeVideoPath('DirectStream'))
# Set playmethod property # Set playmethod property
window('emby_%s.playmethod' % playurl, "DirectStream") window('plex_%s.playmethod' % playurl, "DirectStream")
else: else:
log.info("File is transcoding.") log.info("File is transcoding.")
@ -64,7 +64,7 @@ class PlayUtils():
'videoQuality': '100' 'videoQuality': '100'
})) }))
# Set playmethod property # Set playmethod property
window('emby_%s.playmethod' % playurl, value="Transcode") window('plex_%s.playmethod' % playurl, value="Transcode")
log.info("The playurl is: %s" % playurl) log.info("The playurl is: %s" % playurl)
return playurl return playurl

View file

@ -5,7 +5,7 @@ import string
import xbmc import xbmc
import embydb_functions as embydb import plexdb_functions as plexdb
############################################################################### ###############################################################################
@ -146,11 +146,11 @@ class jsonClass():
def skipTo(self, plexId, typus): def skipTo(self, plexId, typus):
# playlistId = self.getPlaylistId(tryDecode(xbmc_type(typus))) # playlistId = self.getPlaylistId(tryDecode(xbmc_type(typus)))
# playerId = self. # playerId = self.
with embydb.GetEmbyDB() as emby_db: with plexdb.Get_Plex_DB() as plex_db:
embydb_item = emby_db.getItem_byId(plexId) plexdb_item = plex_db.getItem_byId(plexId)
try: try:
dbid = embydb_item[0] dbid = plexdb_item[0]
mediatype = embydb_item[4] mediatype = plexdb_item[4]
except TypeError: except TypeError:
log.info('Couldnt find item %s in Kodi db' % plexId) log.info('Couldnt find item %s in Kodi db' % plexId)
return return
@ -163,7 +163,7 @@ class jsonClass():
"Access-Control-Allow-Origin": "*", "Access-Control-Allow-Origin": "*",
"X-Plex-Version": self.settings['version'], "X-Plex-Version": self.settings['version'],
"X-Plex-Client-Identifier": self.settings['uuid'], "X-Plex-Client-Identifier": self.settings['uuid'],
"X-Plex-Provides": "player", "X-Plex-Provides": "client,controller,player",
"X-Plex-Product": "PlexKodiConnect", "X-Plex-Product": "PlexKodiConnect",
"X-Plex-Device-Name": self.settings['client_name'], "X-Plex-Device-Name": self.settings['client_name'],
"X-Plex-Platform": "Kodi", "X-Plex-Platform": "Kodi",

View file

@ -72,7 +72,7 @@ class plexgdm:
"Protocol: plex\r\n" "Protocol: plex\r\n"
"Protocol-Version: 1\r\n" "Protocol-Version: 1\r\n"
"Protocol-Capabilities: timeline,playback,navigation," "Protocol-Capabilities: timeline,playback,navigation,"
"mirror,playqueues\r\n" "playqueues\r\n"
"Device-Class: HTPC" "Device-Class: HTPC"
) % ( ) % (
options['uuid'], options['uuid'],

View file

@ -15,7 +15,7 @@ log = logging.getLogger("PLEX."+__name__)
class SubscriptionManager: class SubscriptionManager:
def __init__(self, jsonClass, RequestMgr, player, playlist): def __init__(self, jsonClass, RequestMgr, player, mgr):
self.serverlist = [] self.serverlist = []
self.subscribers = {} self.subscribers = {}
self.info = {} self.info = {}
@ -36,7 +36,7 @@ class SubscriptionManager:
self.playerprops = {} self.playerprops = {}
self.doUtils = downloadutils.DownloadUtils().downloadUrl self.doUtils = downloadutils.DownloadUtils().downloadUrl
self.xbmcplayer = player self.xbmcplayer = player
self.playlist = playlist self.playqueue = mgr.playqueue
self.js = jsonClass self.js = jsonClass
self.RequestMgr = RequestMgr self.RequestMgr = RequestMgr
@ -160,8 +160,8 @@ class SubscriptionManager:
with threading.RLock(): with threading.RLock():
for sub in self.subscribers.values(): for sub in self.subscribers.values():
sub.send_update(msg, len(players) == 0) sub.send_update(msg, len(players) == 0)
self.notifyServer(players) self.notifyServer(players)
self.lastplayers = players self.lastplayers = players
return True return True
def notifyServer(self, players): def notifyServer(self, players):
@ -231,6 +231,8 @@ class SubscriptionManager:
def getPlayerProperties(self, playerid): def getPlayerProperties(self, playerid):
try: try:
# Get the playqueue
playqueue = self.playqueue.playqueues[playerid]
# get info from the player # get info from the player
props = self.js.jsonrpc( props = self.js.jsonrpc(
"Player.GetProperties", "Player.GetProperties",
@ -248,18 +250,16 @@ class SubscriptionManager:
'shuffle': ("0", "1")[props.get('shuffled', False)], 'shuffle': ("0", "1")[props.get('shuffled', False)],
'repeat': pf.getPlexRepeat(props.get('repeat')), 'repeat': pf.getPlexRepeat(props.get('repeat')),
} }
if self.playlist is not None: if playqueue.ID is not None:
if self.playlist.QueueId() is not None: info['playQueueID'] = playqueue.ID
info['playQueueID'] = self.playlist.QueueId() info['playQueueVersion'] = playqueue.version
info['playQueueVersion'] = self.playlist.PlayQueueVersion() # Get the playlist position
info['guid'] = self.playlist.Guid() pos = self.js.jsonrpc(
# Get the playlist position "Player.GetProperties",
pos = self.js.jsonrpc( {"playerid": playerid,
"Player.GetProperties", "properties": ["position"]})['position']
{"playerid": playerid, info['playQueueItemID'] = playqueue.items[pos].ID
"properties": ["position"]}) info['guid'] = playqueue.items[pos].guid
info['playQueueItemID'] = \
self.playlist.getQueueIdFromPosition(pos['position'])
except: except:
import traceback import traceback
log.error("Traceback:\n%s" % traceback.format_exc()) log.error("Traceback:\n%s" % traceback.format_exc())

View file

@ -0,0 +1,391 @@
# -*- coding: utf-8 -*-
###############################################################################
from utils import kodiSQL
import logging
###############################################################################
log = logging.getLogger("PLEX."+__name__)
###############################################################################
class Get_Plex_DB():
"""
Usage: with Get_Plex_DB() as plex_db:
plex_db.do_something()
On exiting "with" (no matter what), commits get automatically committed
and the db gets closed
"""
def __enter__(self):
self.plexconn = kodiSQL('plex')
return Plex_DB_Functions(self.plexconn.cursor())
def __exit__(self, type, value, traceback):
self.plexconn.commit()
self.plexconn.close()
class Plex_DB_Functions():
def __init__(self, plexcursor):
self.plexcursor = plexcursor
def getViews(self):
"""
Returns a list of view_id
"""
views = []
query = '''
SELECT view_id
FROM view
'''
self.plexcursor.execute(query)
rows = self.plexcursor.fetchall()
for row in rows:
views.append(row[0])
return views
def getAllViewInfo(self):
"""
Returns a list of dicts:
{'id': view_id, 'name': view_name, 'itemtype': kodi_type}
"""
plexcursor = self.plexcursor
views = []
query = '''
SELECT view_id, view_name, kodi_type
FROM view
'''
plexcursor.execute(query)
rows = plexcursor.fetchall()
for row in rows:
views.append({'id': row[0],
'name': row[1],
'itemtype': row[2]})
return views
def getView_byId(self, view_id):
"""
Returns tuple (view_name, kodi_type, kodi_tagid) for view_id
"""
query = '''
SELECT view_name, kodi_type, kodi_tagid
FROM view
WHERE view_id = ?
'''
self.plexcursor.execute(query, (view_id,))
view = self.plexcursor.fetchone()
return view
def getView_byType(self, kodi_type):
"""
Returns a list of dicts for kodi_type:
{'id': view_id, 'name': view_name, 'itemtype': kodi_type}
"""
views = []
query = '''
SELECT view_id, view_name, kodi_type
FROM view
WHERE kodi_type = ?
'''
self.plexcursor.execute(query, (kodi_type,))
rows = self.plexcursor.fetchall()
for row in rows:
views.append({
'id': row[0],
'name': row[1],
'itemtype': row[2]
})
return views
def getView_byName(self, view_name):
"""
Returns the view_id for view_name (or None)
"""
query = '''
SELECT view_id
FROM view
WHERE view_name = ?
'''
self.plexcursor.execute(query, (view_name,))
try:
view = self.plexcursor.fetchone()[0]
except TypeError:
view = None
return view
def addView(self, view_id, view_name, kodi_type, kodi_tagid):
"""
Appends an entry to the view table
"""
query = '''
INSERT INTO view(
view_id, view_name, kodi_type, kodi_tagid)
VALUES (?, ?, ?, ?)
'''
self.plexcursor.execute(query,
(view_id, view_name, kodi_type, kodi_tagid))
def updateView(self, view_name, kodi_tagid, view_id):
"""
Updates the view_id with view_name and kodi_tagid
"""
query = '''
UPDATE view
SET view_name = ?, kodi_tagid = ?
WHERE view_id = ?
'''
self.plexcursor.execute(query, (view_name, kodi_tagid, view_id))
def removeView(self, view_id):
query = '''
DELETE FROM view
WHERE view_id = ?
'''
self.plexcursor.execute(query, (view_id,))
def getItem_byFileId(self, kodi_fileid, kodi_type):
"""
Returns plex_id for kodi_fileid and kodi_type
None if not found
"""
query = '''
SELECT plex_id
FROM plex
WHERE kodi_fileid = ? AND kodi_type = ?
'''
try:
self.plexcursor.execute(query, (kodi_fileid, kodi_type))
item = self.plexcursor.fetchone()[0]
return item
except:
return None
def getMusicItem_byFileId(self, kodi_id, kodi_type):
"""
Returns the plex_id for kodi_id and kodi_type
None if not found
"""
query = '''
SELECT plex_id
FROM plex
WHERE kodi_id = ? AND kodi_type = ?
'''
try:
self.plexcursor.execute(query, (kodi_id, kodi_type))
item = self.plexcursor.fetchone()[0]
return item
except:
return None
def getItem_byId(self, plex_id):
"""
For plex_id, returns the tuple
(kodi_id, kodi_fileid, kodi_pathid, parent_id, kodi_type, plex_type)
None if not found
"""
query = '''
SELECT kodi_id, kodi_fileid, kodi_pathid,
parent_id, kodi_type, plex_type
FROM plex
WHERE plex_id = ?
'''
try:
self.plexcursor.execute(query, (plex_id,))
item = self.plexcursor.fetchone()
return item
except:
return None
def getItem_byWildId(self, plex_id):
"""
Returns a list of tuples (kodi_id, kodi_type) for plex_id (% appended)
"""
query = '''
SELECT kodi_id, kodi_type
FROM plex
WHERE plex_id LIKE ?
'''
self.plexcursor.execute(query, (plex_id+"%",))
return self.plexcursor.fetchall()
def getItem_byView(self, view_id):
"""
Returns kodi_id for view_id
"""
query = '''
SELECT kodi_id
FROM plex
WHERE view_id = ?
'''
self.plexcursor.execute(query, (view_id,))
return self.plexcursor.fetchall()
def getItem_byKodiId(self, kodi_id, kodi_type):
"""
Returns the tuple (plex_id, parent_id) for kodi_id and kodi_type
"""
query = '''
SELECT plex_id, parent_id
FROM plex
WHERE kodi_id = ?
AND kodi_type = ?
'''
self.plexcursor.execute(query, (kodi_id, kodi_type,))
return self.plexcursor.fetchone()
def getItem_byParentId(self, parent_id, kodi_type):
"""
Returns the tuple (plex_id, kodi_id, kodi_fileid) for parent_id,
kodi_type
"""
query = '''
SELECT plex_id, kodi_id, kodi_fileid
FROM plex
WHERE parent_id = ?
AND kodi_type = ?"
'''
self.plexcursor.execute(query, (parent_id, kodi_type,))
return self.plexcursor.fetchall()
def getItemId_byParentId(self, parent_id, kodi_type):
"""
Returns the tuple (plex_id, kodi_id) for parent_id, kodi_type
"""
query = '''
SELECT plex_id, kodi_id
FROM plex
WHERE parent_id = ?
AND kodi_type = ?
'''
self.plexcursor.execute(query, (parent_id, kodi_type,))
return self.plexcursor.fetchall()
def getChecksum(self, plex_type):
"""
Returns a list of tuples (plex_id, checksum) for plex_type
"""
query = '''
SELECT plex_id, checksum
FROM plex
WHERE plex_type = ?
'''
self.plexcursor.execute(query, (plex_type,))
return self.plexcursor.fetchall()
def getMediaType_byId(self, plex_id):
"""
Returns plex_type for plex_id
Or None if not found
"""
query = '''
SELECT plex_type
FROM plex
WHERE plex_id = ?
'''
self.plexcursor.execute(query, (plex_id,))
try:
itemtype = self.plexcursor.fetchone()[0]
except TypeError:
itemtype = None
return itemtype
def addReference(self, plex_id, plex_type, kodi_id, kodi_type,
kodi_fileid=None, kodi_pathid=None, parent_id=None,
checksum=None, view_id=None):
"""
Appends or replaces an entry into the plex table
"""
query = '''
INSERT OR REPLACE INTO plex(
plex_id, kodi_id, kodi_fileid, kodi_pathid, plex_type,
kodi_type, parent_id, checksum, view_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
'''
self.plexcursor.execute(query, (plex_id, kodi_id, kodi_fileid,
kodi_pathid, plex_type, kodi_type,
parent_id, checksum, view_id))
def updateReference(self, plex_id, checksum):
"""
Updates checksum for plex_id
"""
query = "UPDATE plex SET checksum = ? WHERE plex_id = ?"
self.plexcursor.execute(query, (checksum, plex_id))
def updateParentId(self, plexid, parent_kodiid):
"""
Updates parent_id for plex_id
"""
query = "UPDATE plex SET parent_id = ? WHERE plex_id = ?"
self.plexcursor.execute(query, (parent_kodiid, plexid))
def removeItems_byParentId(self, parent_id, kodi_type):
"""
Removes all entries with parent_id and kodi_type
"""
query = '''
DELETE FROM plex
WHERE parent_id = ?
AND kodi_type = ?
'''
self.plexcursor.execute(query, (parent_id, kodi_type,))
def removeItem_byKodiId(self, kodi_id, kodi_type):
"""
Removes the one entry with kodi_id and kodi_type
"""
query = '''
DELETE FROM plex
WHERE kodi_id = ?
AND kodi_type = ?
'''
self.plexcursor.execute(query, (kodi_id, kodi_type,))
def removeItem(self, plex_id):
"""
Removes the one entry with plex_id
"""
query = "DELETE FROM plex WHERE plex_id = ?"
self.plexcursor.execute(query, (plex_id,))
def removeWildItem(self, plex_id):
"""
Removes all entries with plex_id with % added
"""
query = "DELETE FROM plex WHERE plex_id LIKE ?"
self.plexcursor.execute(query, (plex_id+"%",))
def itemsByType(self, plex_type):
"""
Returns a list of dicts for plex_type:
{
'plexId': plex_id
'kodiId': kodi_id
'kodi_type': kodi_type
'plex_type': plex_type
}
"""
query = '''
SELECT plex_id, kodi_id, kodi_type
FROM plex
WHERE plex_type = ?
'''
self.plexcursor.execute(query, (plex_type, ))
result = []
for row in self.plexcursor.fetchall():
result.append({
'plexId': row[0],
'kodiId': row[1],
'kodi_type': row[2],
'plex_type': plex_type
})
return result

View file

@ -101,7 +101,7 @@ class Read_EmbyServer():
viewId = view['Id'] viewId = view['Id']
# Compare to view table in emby database # Compare to view table in emby database
emby = kodiSQL('emby') emby = kodiSQL('plex')
cursor_emby = emby.cursor() cursor_emby = emby.cursor()
query = ' '.join(( query = ' '.join((

View file

@ -32,8 +32,10 @@ class UserClient(threading.Thread):
# Borg - multiple instances, shared state # Borg - multiple instances, shared state
__shared_state = {} __shared_state = {}
def __init__(self): def __init__(self, callback=None):
self.__dict__ = self.__shared_state self.__dict__ = self.__shared_state
if callback is not None:
self.mgr = callback
self.auth = True self.auth = True
self.retry = 0 self.retry = 0

View file

@ -15,7 +15,6 @@ from functools import wraps
from calendar import timegm from calendar import timegm
import os import os
import xbmc import xbmc
import xbmcaddon import xbmcaddon
import xbmcgui import xbmcgui
@ -57,6 +56,24 @@ def window(property, value=None, clear=False, windowid=10000):
return tryDecode(win.getProperty(property)) return tryDecode(win.getProperty(property))
def pickl_window(property, value=None, clear=False, windowid=10000):
"""
Get or set window property - thread safe! For use with Pickle
Property and value must be string
"""
if windowid != 10000:
win = xbmcgui.Window(windowid)
else:
win = WINDOW
if clear:
win.clearProperty(property)
elif value is not None:
win.setProperty(property, value)
else:
return win.getProperty(property)
def settings(setting, value=None): def settings(setting, value=None):
""" """
Get or add addon setting. Returns unicode Get or add addon setting. Returns unicode
@ -134,21 +151,21 @@ def tryDecode(string, encoding='utf-8'):
def DateToKodi(stamp): def DateToKodi(stamp):
""" """
converts a Unix time stamp (seconds passed sinceJanuary 1 1970) to a converts a Unix time stamp (seconds passed sinceJanuary 1 1970) to a
propper, human-readable time stamp used by Kodi propper, human-readable time stamp used by Kodi
Output: Y-m-d h:m:s = 2009-04-05 23:16:04 Output: Y-m-d h:m:s = 2009-04-05 23:16:04
None if an error was encountered None if an error was encountered
""" """
try: try:
stamp = float(stamp) + float(window('kodiplextimeoffset')) stamp = float(stamp) + float(window('kodiplextimeoffset'))
date_time = time.localtime(stamp) date_time = time.localtime(stamp)
localdate = time.strftime('%Y-%m-%d %H:%M:%S', date_time) localdate = time.strftime('%Y-%m-%d %H:%M:%S', date_time)
except: except:
localdate = None localdate = None
return localdate return localdate
def IfExists(path): def IfExists(path):
@ -200,8 +217,8 @@ def getUnixTimestamp(secondsIntoTheFuture=None):
def kodiSQL(media_type="video"): def kodiSQL(media_type="video"):
if media_type == "emby": if media_type == "plex":
dbPath = tryDecode(xbmc.translatePath("special://database/emby.db")) dbPath = tryDecode(xbmc.translatePath("special://database/plex.db"))
elif media_type == "music": elif media_type == "music":
dbPath = getKodiMusicDBPath() dbPath = getKodiMusicDBPath()
elif media_type == "texture": elif media_type == "texture":
@ -346,7 +363,7 @@ def reset():
# Wipe the Plex database # Wipe the Plex database
log.info("Resetting the Plex database.") log.info("Resetting the Plex database.")
connection = kodiSQL('emby') connection = kodiSQL('plex')
cursor = connection.cursor() cursor = connection.cursor()
cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"') cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"')
rows = cursor.fetchall() rows = cursor.fetchall()
@ -939,9 +956,33 @@ def ThreadMethods(cls):
return cls return cls
class Lock_Function:
"""
Decorator for class methods and functions to lock them with lock.
Initialize this class first
lockfunction = Lock_Function(lock), where lock is a threading.Lock() object
To then lock a function or method:
@lockfunction.lockthis
def some_function(args, kwargs)
"""
def __init__(self, lock):
self.lock = lock
def lockthis(self, func):
@wraps(func)
def wrapper(*args, **kwargs):
with self.lock:
result = func(*args, **kwargs)
return result
return wrapper
############################################################################### ###############################################################################
# UNUSED METHODS # UNUSED METHODS
def changePlayState(itemType, kodiId, playCount, lastplayed): def changePlayState(itemType, kodiId, playCount, lastplayed):
""" """
YET UNUSED YET UNUSED
@ -985,3 +1026,27 @@ def changePlayState(itemType, kodiId, playCount, lastplayed):
result = json.loads(result) result = json.loads(result)
result = result.get('result') result = result.get('result')
log.debug("JSON result was: %s" % result) log.debug("JSON result was: %s" % result)
class JSONRPC(object):
id_ = 1
jsonrpc = "2.0"
def __init__(self, method, **kwargs):
self.method = method
for arg in kwargs: # id_(int), jsonrpc(str)
self.arg = arg
def _query(self):
query = {
'jsonrpc': self.jsonrpc,
'id': self.id_,
'method': self.method,
}
if self.params is not None:
query['params'] = self.params
return json.dumps(query)
def execute(self, params=None):
self.params = params
return json.loads(xbmc.executeJSONRPC(self._query()))

View file

@ -2,13 +2,13 @@
############################################################################### ###############################################################################
import logging import logging
import json
import threading
import Queue
import websocket import websocket
import ssl from json import loads
from threading import Thread
from Queue import Queue
from ssl import CERT_NONE
import xbmc from xbmc import sleep
from utils import window, settings, ThreadMethodsAdditionalSuspend, \ from utils import window, settings, ThreadMethodsAdditionalSuspend, \
ThreadMethods ThreadMethods
@ -22,21 +22,23 @@ log = logging.getLogger("PLEX."+__name__)
@ThreadMethodsAdditionalSuspend('suspend_LibraryThread') @ThreadMethodsAdditionalSuspend('suspend_LibraryThread')
@ThreadMethods @ThreadMethods
class WebSocket(threading.Thread): class WebSocket(Thread):
opcode_data = (websocket.ABNF.OPCODE_TEXT, websocket.ABNF.OPCODE_BINARY) opcode_data = (websocket.ABNF.OPCODE_TEXT, websocket.ABNF.OPCODE_BINARY)
def __init__(self, queue): def __init__(self, callback=None):
if callback is not None:
self.mgr = callback
self.ws = None self.ws = None
# Communication with librarysync # Communication with librarysync
self.queue = queue self.queue = Queue()
threading.Thread.__init__(self) Thread.__init__(self)
def process(self, opcode, message): def process(self, opcode, message):
if opcode not in self.opcode_data: if opcode not in self.opcode_data:
return False return False
try: try:
message = json.loads(message) message = loads(message)
except Exception as ex: except Exception as ex:
log.error('Error decoding message from websocket: %s' % ex) log.error('Error decoding message from websocket: %s' % ex)
log.error(message) log.error(message)
@ -57,13 +59,8 @@ class WebSocket(threading.Thread):
return True return True
# Put PMS message on queue and let libsync take care of it # Put PMS message on queue and let libsync take care of it
try: self.queue.put(message)
self.queue.put(message) return True
return True
except Queue.Full:
# Queue only takes 200 messages. No worries if we miss one or two
log.info('Queue is full, dropping PMS message %s' % message)
return False
def receive(self, ws): def receive(self, ws):
# Not connected yet # Not connected yet
@ -97,7 +94,7 @@ class WebSocket(threading.Thread):
uri += '?X-Plex-Token=%s' % token uri += '?X-Plex-Token=%s' % token
sslopt = {} sslopt = {}
if settings('sslverify') == "false": if settings('sslverify') == "false":
sslopt["cert_reqs"] = ssl.CERT_NONE sslopt["cert_reqs"] = CERT_NONE
log.debug("Uri: %s, sslopt: %s" % (uri, sslopt)) log.debug("Uri: %s, sslopt: %s" % (uri, sslopt))
return uri, sslopt return uri, sslopt
@ -122,7 +119,7 @@ class WebSocket(threading.Thread):
# Abort was requested while waiting. We should exit # Abort was requested while waiting. We should exit
log.info("##===---- WebSocketClient Stopped ----===##") log.info("##===---- WebSocketClient Stopped ----===##")
return return
xbmc.sleep(1000) sleep(1000)
try: try:
self.process(*self.receive(self.ws)) self.process(*self.receive(self.ws))
except websocket.WebSocketTimeoutException: except websocket.WebSocketTimeoutException:
@ -148,11 +145,11 @@ class WebSocket(threading.Thread):
"declaring the connection dead") "declaring the connection dead")
window('plex_online', value='false') window('plex_online', value='false')
counter = 0 counter = 0
xbmc.sleep(1000) sleep(1000)
except websocket.WebSocketTimeoutException: except websocket.WebSocketTimeoutException:
log.info("timeout while connecting, trying again") log.info("timeout while connecting, trying again")
self.ws = None self.ws = None
xbmc.sleep(1000) sleep(1000)
except websocket.WebSocketException as e: except websocket.WebSocketException as e:
log.info('WebSocketException: %s' % e) log.info('WebSocketException: %s' % e)
if 'Handshake Status 401' in e.args: if 'Handshake Status 401' in e.args:
@ -162,14 +159,14 @@ class WebSocket(threading.Thread):
'WebSocketClient now') 'WebSocketClient now')
break break
self.ws = None self.ws = None
xbmc.sleep(1000) sleep(1000)
except Exception as e: except Exception as e:
log.error("Unknown exception encountered in connecting: %s" log.error("Unknown exception encountered in connecting: %s"
% e) % e)
import traceback import traceback
log.error("Traceback:\n%s" % traceback.format_exc()) log.error("Traceback:\n%s" % traceback.format_exc())
self.ws = None self.ws = None
xbmc.sleep(1000) sleep(1000)
else: else:
counter = 0 counter = 0
handshake_counter = 0 handshake_counter = 0

View file

@ -87,7 +87,7 @@
<category label="30516"><!-- Playback --> <category label="30516"><!-- Playback -->
<setting type="sep" /> <setting type="sep" />
<setting id="enableCinema" type="bool" label="30518" default="true" /> <setting id="enableCinema" type="bool" label="30518" default="false" />
<setting id="askCinema" type="bool" label="30519" default="false" visible="eq(-1,true)" subsetting="true" /> <setting id="askCinema" type="bool" label="30519" default="false" visible="eq(-1,true)" subsetting="true" />
<setting id="trailerNumber" type="slider" label="39000" default="3" visible="eq(-2,true)" range="1,1,15" option="int" /> <setting id="trailerNumber" type="slider" label="39000" default="3" visible="eq(-2,true)" range="1,1,15" option="int" />
<setting id="pickPlexSubtitles" type="bool" label="39075" default="true" /> <setting id="pickPlexSubtitles" type="bool" label="39075" default="true" />

View file

@ -5,7 +5,6 @@
import logging import logging
import os import os
import sys import sys
import Queue
import xbmc import xbmc
import xbmcaddon import xbmcaddon
@ -33,17 +32,20 @@ sys.path.append(_base_resource)
############################################################################### ###############################################################################
from utils import settings, window, language as lang from utils import settings, window, language as lang
import userclient from userclient import UserClient
import clientinfo import clientinfo
import initialsetup import initialsetup
import kodimonitor from kodimonitor import KodiMonitor
import librarysync from librarysync import LibrarySync
import videonodes import videonodes
import websocket_client as wsc from websocket_client import WebSocket
import downloadutils import downloadutils
from playqueue import Playqueue
import PlexAPI import PlexAPI
import PlexCompanion from PlexCompanion import PlexCompanion
from monitor_kodi_play import Monitor_Kodi_Play
from playback_starter import Playback_Starter
############################################################################### ###############################################################################
@ -61,11 +63,19 @@ class Service():
server_online = True server_online = True
warn_auth = True warn_auth = True
userclient_running = False user = None
websocket_running = False ws = None
library = None
plexCompanion = None
playqueue = None
user_running = False
ws_running = False
library_running = False library_running = False
kodimonitor_running = False
plexCompanion_running = False plexCompanion_running = False
playqueue_running = False
kodimonitor_running = False
playback_starter_running = False
def __init__(self): def __init__(self):
@ -96,13 +106,14 @@ class Service():
"plex_online", "plex_serverStatus", "plex_onWake", "plex_online", "plex_serverStatus", "plex_onWake",
"plex_dbCheck", "plex_kodiScan", "plex_dbCheck", "plex_kodiScan",
"plex_shouldStop", "currUserId", "plex_dbScan", "plex_shouldStop", "currUserId", "plex_dbScan",
"plex_initialScan", "plex_customplaylist", "plex_playbackProps", "plex_initialScan", "plex_customplayqueue", "plex_playbackProps",
"plex_runLibScan", "plex_username", "pms_token", "plex_token", "plex_runLibScan", "plex_username", "pms_token", "plex_token",
"pms_server", "plex_machineIdentifier", "plex_servername", "pms_server", "plex_machineIdentifier", "plex_servername",
"plex_authenticated", "PlexUserImage", "useDirectPaths", "plex_authenticated", "PlexUserImage", "useDirectPaths",
"suspend_LibraryThread", "plex_terminateNow", "suspend_LibraryThread", "plex_terminateNow",
"kodiplextimeoffset", "countError", "countUnauthorized", "kodiplextimeoffset", "countError", "countUnauthorized",
"plex_restricteduser", "plex_allows_mediaDeletion" "plex_restricteduser", "plex_allows_mediaDeletion",
"plex_play_new_item", "plex_result"
] ]
for prop in properties: for prop in properties:
window(prop, clear=True) window(prop, clear=True)
@ -111,7 +122,7 @@ class Service():
videonodes.VideoNodes().clearProperties() videonodes.VideoNodes().clearProperties()
# Set the minimum database version # Set the minimum database version
window('plex_minDBVersion', value="1.1.5") window('plex_minDBVersion', value="1.5.2")
def getLogLevel(self): def getLogLevel(self):
try: try:
@ -126,16 +137,21 @@ class Service():
monitor = self.monitor monitor = self.monitor
kodiProfile = xbmc.translatePath("special://profile") kodiProfile = xbmc.translatePath("special://profile")
# Detect playback start early on
self.monitor_kodi_play = Monitor_Kodi_Play(self)
self.monitor_kodi_play.start()
# Server auto-detect # Server auto-detect
initialsetup.InitialSetup().setup() initialsetup.InitialSetup().setup()
# Queue for background sync # Initialize important threads, handing over self for callback purposes
queue = Queue.Queue() self.user = UserClient(self)
self.ws = WebSocket(self)
self.library = LibrarySync(self)
self.plexCompanion = PlexCompanion(self)
self.playqueue = Playqueue(self)
self.playback_starter = Playback_Starter(self)
# Initialize important threads
user = userclient.UserClient()
ws = wsc.WebSocket(queue)
library = librarysync.LibrarySync(queue)
plx = PlexAPI.PlexAPI() plx = PlexAPI.PlexAPI()
welcome_msg = True welcome_msg = True
@ -157,7 +173,7 @@ class Service():
if window('plex_online') == "true": if window('plex_online') == "true":
# Plex server is online # Plex server is online
# Verify if user is set and has access to the server # Verify if user is set and has access to the server
if (user.currUser is not None) and user.HasAccess: if (self.user.currUser is not None) and self.user.HasAccess:
if not self.kodimonitor_running: if not self.kodimonitor_running:
# Start up events # Start up events
self.warn_auth = True self.warn_auth = True
@ -166,38 +182,46 @@ class Service():
welcome_msg = False welcome_msg = False
xbmcgui.Dialog().notification( xbmcgui.Dialog().notification(
heading=addonName, heading=addonName,
message="%s %s" % (lang(33000), user.currUser), message="%s %s" % (lang(33000),
icon="special://home/addons/plugin.video.plexkodiconnect/icon.png", self.user.currUser),
icon="special://home/addons/plugin."
"video.plexkodiconnect/icon.png",
time=2000, time=2000,
sound=False) sound=False)
# Start monitoring kodi events # Start monitoring kodi events
self.kodimonitor_running = kodimonitor.KodiMonitor() self.kodimonitor_running = KodiMonitor(self)
# Start playqueue client
if not self.playqueue_running:
self.playqueue_running = True
self.playqueue.start()
# Start the Websocket Client # Start the Websocket Client
if not self.websocket_running: if not self.ws_running:
self.websocket_running = True self.ws_running = True
ws.start() self.ws.start()
# Start the syncing thread # Start the syncing thread
if not self.library_running: if not self.library_running:
self.library_running = True self.library_running = True
library.start() self.library.start()
# Start the Plex Companion thread # Start the Plex Companion thread
if not self.plexCompanion_running: if not self.plexCompanion_running:
self.plexCompanion_running = True self.plexCompanion_running = True
plexCompanion = PlexCompanion.PlexCompanion() self.plexCompanion.start()
plexCompanion.start() if not self.playback_starter_running:
self.playback_starter_running = True
self.playback_starter.start()
else: else:
if (user.currUser is None) and self.warn_auth: if (self.user.currUser is None) and self.warn_auth:
# Alert user is not authenticated and suppress future warning # Alert user is not authenticated and suppress future
# warning
self.warn_auth = False self.warn_auth = False
log.warn("Not authenticated yet.") log.warn("Not authenticated yet.")
# User access is restricted. # User access is restricted.
# Keep verifying until access is granted # Keep verifying until access is granted
# unless server goes offline or Kodi is shut down. # unless server goes offline or Kodi is shut down.
while user.HasAccess == False: while self.user.HasAccess is False:
# Verify access with an API call # Verify access with an API call
user.hasAccess() self.user.hasAccess()
if window('plex_online') != "true": if window('plex_online') != "true":
# Server went offline # Server went offline
@ -211,7 +235,7 @@ class Service():
# Wait until Plex server is online # Wait until Plex server is online
# or Kodi is shut down. # or Kodi is shut down.
while not monitor.abortRequested(): while not monitor.abortRequested():
server = user.getServer() server = self.user.getServer()
if server is False: if server is False:
# No server info set in add-on settings # No server info set in add-on settings
pass pass
@ -268,9 +292,9 @@ class Service():
window('suspend_LibraryThread', clear=True) window('suspend_LibraryThread', clear=True)
# Start the userclient thread # Start the userclient thread
if not self.userclient_running: if not self.user_running:
self.userclient_running = True self.user_running = True
user.start() self.user.start()
break break
@ -286,27 +310,22 @@ class Service():
# Tell all threads to terminate (e.g. several lib sync threads) # Tell all threads to terminate (e.g. several lib sync threads)
window('plex_terminateNow', value='true') window('plex_terminateNow', value='true')
try: try:
plexCompanion.stopThread() self.plexCompanion.stopThread()
except: except:
log.warn('plexCompanion already shut down') log.warn('plexCompanion already shut down')
try: try:
library.stopThread() self.library.stopThread()
except: except:
log.warn('Library sync already shut down') log.warn('Library sync already shut down')
try: try:
ws.stopThread() self.ws.stopThread()
except: except:
log.warn('Websocket client already shut down') log.warn('Websocket client already shut down')
try: try:
user.stopThread() self.user.stopThread()
except: except:
log.warn('User client already shut down') log.warn('User client already shut down')
try: try:
downloadutils.DownloadUtils().stopSession() downloadutils.DownloadUtils().stopSession()
except: except: