From 146f063fc930b9a9df1ffb51149566b04619d7cf Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Mon, 2 Jan 2017 14:07:24 +0100 Subject: [PATCH] Playqueues overhaul continued --- addon.xml | 2 - default.py | 95 ++++---- resources/lib/PKC_listitem.py | 334 ++++++++++++++++++++++++++ resources/lib/PlexAPI.py | 8 +- resources/lib/PlexCompanion.py | 18 +- resources/lib/context_entry.py | 2 +- resources/lib/dialogs/usersconnect.py | 2 +- resources/lib/entrypoint.py | 76 +----- resources/lib/itemtypes.py | 10 +- resources/lib/kodidb_functions.py | 2 +- resources/lib/kodimonitor.py | 14 -- resources/lib/monitor_kodi_play.py | 41 ++++ resources/lib/pickler.py | 44 ++++ resources/lib/playback_starter.py | 92 +++++++ resources/lib/playbackutils.py | 274 +++++++++++---------- resources/lib/playlist_func.py | 280 +++++++++++++-------- resources/lib/playqueue.py | 286 ++++++++-------------- resources/lib/utils.py | 18 ++ service.py | 14 +- 19 files changed, 1049 insertions(+), 563 deletions(-) create mode 100644 resources/lib/PKC_listitem.py create mode 100644 resources/lib/monitor_kodi_play.py create mode 100644 resources/lib/pickler.py create mode 100644 resources/lib/playback_starter.py diff --git a/addon.xml b/addon.xml index 85d4d694..192c6930 100644 --- a/addon.xml +++ b/addon.xml @@ -6,8 +6,6 @@ - - diff --git a/default.py b/default.py index 8dc00b25..235734e3 100644 --- a/default.py +++ b/default.py @@ -3,36 +3,38 @@ ############################################################################### import logging -import os -import sys -import urlparse +from os import path as os_path +from sys import path as sys_path, argv +from urlparse import parse_qsl -import xbmc -import xbmcaddon -import xbmcgui +from xbmc import translatePath, sleep, executebuiltin +from xbmcaddon import Addon +from xbmcgui import ListItem, Dialog +from xbmcplugin import setResolvedUrl - -_addon = xbmcaddon.Addon(id='plugin.video.plexkodiconnect') +_addon = Addon(id='plugin.video.plexkodiconnect') try: _addon_path = _addon.getAddonInfo('path').decode('utf-8') except TypeError: _addon_path = _addon.getAddonInfo('path').decode() try: - _base_resource = xbmc.translatePath(os.path.join( + _base_resource = translatePath(os_path.join( _addon_path, 'resources', 'lib')).decode('utf-8') except TypeError: - _base_resource = xbmc.translatePath(os.path.join( + _base_resource = translatePath(os_path.join( _addon_path, 'resources', 'lib')).decode() -sys.path.append(_base_resource) +sys_path.append(_base_resource) ############################################################################### 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(): # MAIN ENTRY POINT - #@utils.profiling() + # @utils.profiling() def __init__(self): + log.debug("Full sys.argv received: %s" % ARGV) # Parse parameters - log.warn("Full sys.argv received: %s" % sys.argv) - base_url = sys.argv[0] - params = urlparse.parse_qs(sys.argv[2][1:]) + params = dict(parse_qsl(ARGV[2][1:])) try: - mode = params['mode'][0] + mode = params['mode'] itemid = params.get('id', '') - if itemid: - try: - itemid = itemid[0] - except: - pass except: - params = {} 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 = { - - 'reset': utils.reset, + 'reset': reset, 'resetauth': entrypoint.resetAuth, - 'play': entrypoint.doPlayback, - 'passwords': utils.passwordsXML, + 'passwords': passwordsXML, 'channels': entrypoint.BrowseChannels, 'channelsfolder': entrypoint.BrowseChannels, 'browsecontent': entrypoint.BrowseContent, @@ -79,7 +94,6 @@ class Main(): 'inprogressepisodes': entrypoint.getInProgressEpisodes, 'recentepisodes': entrypoint.getRecentEpisodes, 'refreshplaylist': entrypoint.refreshPlaylist, - 'companion': entrypoint.plexCompanion, 'switchuser': entrypoint.switchPlexUser, 'deviceid': entrypoint.resetDeviceId, 'delete': entrypoint.deleteItem, @@ -92,8 +106,8 @@ class Main(): 'playwatchlater': entrypoint.playWatchLater } - if "/extrafanart" in sys.argv[0]: - plexpath = sys.argv[2][1:] + if "/extrafanart" in ARGV[0]: + plexpath = ARGV[2][1:] plexid = params.get('id', [""])[0] entrypoint.getExtraFanArt(plexid, plexpath) entrypoint.getVideoFiles(plexid, plexpath) @@ -101,11 +115,11 @@ class Main(): if mode == 'fanart': 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 - if ("/Extras" in sys.argv[0] or "/VideoFiles" in sys.argv[0] or - "/Extras" in sys.argv[2]): + if ("/Extras" in ARGV[0] or "/VideoFiles" in ARGV[0] or + "/Extras" in ARGV[2]): plexId = params.get('id', [None])[0] entrypoint.getVideoFiles(plexId, params) @@ -143,7 +157,7 @@ class Main(): folderid = params['folderid'][0] modes[mode](itemid, folderid) elif mode == "companion": - modes[mode](itemid, params=sys.argv[2]) + modes[mode](itemid, params=ARGV[2]) elif mode == 'playwatchlater': modes[mode](params.get('id')[0], params.get('viewOffset')[0]) else: @@ -151,25 +165,24 @@ class Main(): else: # Other functions if mode == "settings": - xbmc.executebuiltin('Addon.OpenSettings(plugin.video.plexkodiconnect)') + executebuiltin('Addon.OpenSettings(plugin.video.plexkodiconnect)') 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 - xbmcgui.Dialog().ok( + Dialog().ok( "PlexKodiConnect", "Unable to run the sync, the add-on is not connected " "to a Plex server.") log.error("Not connected to a PMS.") else: if mode == 'repair': - utils.window('plex_runLibScan', value="repair") + window('plex_runLibScan', value="repair") log.info("Requesting repair lib sync") elif mode == 'manualsync': log.info("Requesting full library scan") - utils.window('plex_runLibScan', value="full") - + window('plex_runLibScan', value="full") elif mode == "texturecache": - utils.window('plex_runLibScan', value='del_textures') + window('plex_runLibScan', value='del_textures') else: entrypoint.doMainListing() diff --git a/resources/lib/PKC_listitem.py b/resources/lib/PKC_listitem.py new file mode 100644 index 00000000..2bb92a21 --- /dev/null +++ b/resources/lib/PKC_listitem.py @@ -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],)) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index 06aa9484..fa8ca2d1 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -2393,7 +2393,7 @@ class API(): # listItem.setProperty('isPlayable', 'true') # listItem.setProperty('isFolder', 'true') # Further stuff - listItem.setIconImage('DefaultPicture.png') + listItem.setArt({'icon': 'DefaultPicture.png'}) return listItem def _createVideoListItem(self, @@ -2456,14 +2456,14 @@ class API(): "s%.2de%.2d" % (season, episode)) if appendSxxExx is True: title = "S%.2dE%.2d - %s" % (season, episode, title) - listItem.setIconImage('DefaultTVShows.png') + listItem.setArt({'icon': 'DefaultTVShows.png'}) if appendShowTitle is True: title = "%s - %s " % (show, title) elif typus == "movie": - listItem.setIconImage('DefaultMovies.png') + listItem.setArt({'icon': 'DefaultMovies.png'}) else: # E.g. clips, trailers, ... - listItem.setIconImage('DefaultVideo.png') + listItem.setArt({'icon': 'DefaultVideo.png'}) plexId = self.getRatingKey() listItem.setProperty('plexid', plexId) diff --git a/resources/lib/PlexCompanion.py b/resources/lib/PlexCompanion.py index 130cbb22..8af5687b 100644 --- a/resources/lib/PlexCompanion.py +++ b/resources/lib/PlexCompanion.py @@ -85,19 +85,11 @@ class PlexCompanion(Thread): return playqueue = self.mgr.playqueue.get_playqueue_from_type( data['type']) - if ID != playqueue.ID: - # playqueue changed somehow - self.mgr.playqueue.update_playqueue_from_PMS( - playqueue, - ID, - query.get('repeat'), - data.get('offset')) - else: - # No change to the playqueue - self.mgr.playqueue.start_playqueue_initiated_by_companion( - playqueue, - query.get('repeat'), - data.get('offset')) + self.mgr.playqueue.update_playqueue_from_PMS( + playqueue, + ID, + repeat=query.get('repeat'), + offset=data.get('offset')) def run(self): httpd = False diff --git a/resources/lib/context_entry.py b/resources/lib/context_entry.py index d5996b9e..f9772bfc 100644 --- a/resources/lib/context_entry.py +++ b/resources/lib/context_entry.py @@ -212,6 +212,6 @@ class ContextMenu(object): 'mode': "play" } from urllib import urlencode - handle = ("plugin://plugin.video.plexkodiconnect.movies?%s" + handle = ("plugin://plugin.video.plexkodiconnect/movies?%s" % urlencode(params)) xbmc.executebuiltin('RunPlugin(%s)' % handle) diff --git a/resources/lib/dialogs/usersconnect.py b/resources/lib/dialogs/usersconnect.py index 770b0a2c..ed535c98 100644 --- a/resources/lib/dialogs/usersconnect.py +++ b/resources/lib/dialogs/usersconnect.py @@ -67,7 +67,7 @@ class UsersConnect(xbmcgui.WindowXMLDialog): if self.kodi_version > 15: item.setArt({'Icon': user_image}) else: - item.setIconImage(user_image) + item.setArt({'icon': user_image}) return item diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py index de68e73f..27f305fc 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -32,40 +32,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(): """ Lets user choose from list of PMS @@ -180,40 +146,6 @@ def playWatchLater(itemid, viewOffset): return pbutils.PlaybackUtils(xml).play(None, 'plexnode') -def doPlayback(itemid, dbid): - """ - Called only for a SINGLE element, not playQueues - - Always to return with a "setResolvedUrl" - """ - 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: - # Video - return pbutils.PlaybackUtils(xml).play(itemid, dbid) - - ##### DO RESET AUTH ##### def resetAuth(): # User tried login and failed too many times @@ -497,7 +429,7 @@ def createListItemFromEmbyItem(item,art=None,doUtils=downloadutils.DownloadUtils 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') + li.setArt({'icon': 'DefaultPicture.png'}) else: #normal video items li.setProperty('IsPlayable', 'true') @@ -541,7 +473,7 @@ def createListItemFromEmbyItem(item,art=None,doUtils=downloadutils.DownloadUtils if allart.get('Primary'): li.setThumbnailImage(allart.get('Primary')) else: li.setThumbnailImage('DefaultTVShows.png') - li.setIconImage('DefaultTVShows.png') + li.setArt({'icon': '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: @@ -663,7 +595,7 @@ def createListItem(item, appendShowTitle=False, appendSxxExx=False): li.setProperty('totaltime', str(item['resume']['total'])) li.setArt(item['art']) li.setThumbnailImage(item['art'].get('thumb','')) - li.setIconImage('DefaultTVShows.png') + li.setArt({'icon': 'DefaultTVShows.png'}) li.setProperty('dbid', str(item['episodeid'])) li.setProperty('fanart_image', item['art'].get('tvshow.fanart','')) for key, value in item['streamdetails'].iteritems(): @@ -1167,7 +1099,7 @@ def getOnDeck(viewid, mediatype, tagname, limit): 'id': API.getRatingKey(), 'dbid': listitem.getProperty('dbid') } - url = "plugin://plugin.video.plexkodiconnect.tvshows/?%s" \ + url = "plugin://plugin.video.plexkodiconnect/tvshows/?%s" \ % urllib.urlencode(params) xbmcplugin.addDirectoryItem( handle=int(sys.argv[1]), diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index 0ddc8351..04b83d0f 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -407,7 +407,7 @@ class Movies(Items): path = playurl.replace(filename, "") if doIndirect: # Set plugin path and media flags using real filename - path = "plugin://plugin.video.plexkodiconnect.movies/" + path = "plugin://plugin.video.plexkodiconnect/movies/" params = { 'filename': API.getKey(), 'id': itemid, @@ -675,7 +675,7 @@ class TVShows(Items): toplevelpath = "%s/" % dirname(dirname(path)) if doIndirect: # Set plugin path - toplevelpath = "plugin://plugin.video.plexkodiconnect.tvshows/" + toplevelpath = "plugin://plugin.video.plexkodiconnect/tvshows/" path = "%s%s/" % (toplevelpath, itemid) # Add top path @@ -956,7 +956,7 @@ class TVShows(Items): filename = playurl.rsplit('/', 1)[1] else: filename = 'file_not_found.mkv' - path = "plugin://plugin.video.plexkodiconnect.tvshows/%s/" % seriesId + path = "plugin://plugin.video.plexkodiconnect/tvshows/%s/" % seriesId params = { 'filename': tryEncode(filename), 'id': itemid, @@ -966,7 +966,7 @@ class TVShows(Items): filename = "%s?%s" % (path, tryDecode(urlencode(params))) playurl = filename parentPathId = self.kodi_db.addPath( - 'plugin://plugin.video.plexkodiconnect.tvshows/') + 'plugin://plugin.video.plexkodiconnect/tvshows/') # episodes table: # c18 - playurl @@ -1093,7 +1093,7 @@ class TVShows(Items): self.kodi_db.addPlaystate(fileid, resume, runtime, playcount, dateplayed) if not self.directpath and resume: # Create additional entry for widgets. This is only required for plugin/episode. - temppathid = self.kodi_db.getPath("plugin://plugin.video.plexkodiconnect.tvshows/") + temppathid = self.kodi_db.getPath("plugin://plugin.video.plexkodiconnect/tvshows/") tempfileid = self.kodi_db.addFile(filename, temppathid) query = ' '.join(( diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index 683962e2..fd682a07 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -61,7 +61,7 @@ class Kodidb_Functions(): self.cursor.execute( query, ('movies', 'metadata.local', - 'plugin://plugin.video.plexkodiconnect.movies%%')) + 'plugin://plugin.video.plexkodiconnect/movies%%')) def getParentPathId(self, path): """ diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index d165f926..64059295 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -164,20 +164,6 @@ class KodiMonitor(xbmc.Monitor): xbmc.sleep(5000) window('plex_runLibScan', value="full") - elif method == "Playlist.OnClear": - pass - - elif method == "Playlist.OnAdd": - # User (or PKC) manipulated Kodi playlist - # Data : {u'item': {u'type': u'movie', u'id': 3}, u'playlistid': 1, - # u'position': 0} - self.playqueue.kodi_onadd(data) - - elif method == "Playlist.OnRemove": - # User (or PKC) deleted a playlist item - # Data: {u'position': 2, u'playlistid': 1} - self.playqueue.kodi_onremove(data) - def PlayBackStart(self, data): """ Called whenever a playback is started diff --git a/resources/lib/monitor_kodi_play.py b/resources/lib/monitor_kodi_play.py new file mode 100644 index 00000000..b7968eeb --- /dev/null +++ b/resources/lib/monitor_kodi_play.py @@ -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 ##===----") diff --git a/resources/lib/pickler.py b/resources/lib/pickler.py new file mode 100644 index 00000000..9bd73bec --- /dev/null +++ b/resources/lib/pickler.py @@ -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 diff --git a/resources/lib/playback_starter.py b/resources/lib/playback_starter.py new file mode 100644 index 00000000..ddc10afe --- /dev/null +++ b/resources/lib/playback_starter.py @@ -0,0 +1,92 @@ +# -*- 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 +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) + if xml[0].attrib.get('type') == 'photo': + # Photo + result = Playback_Successful() + listitem = PKC_ListItem() + api = API(xml[0]) + listitem = api.CreateListItemFromPlexItem(listitem) + api.AddStreamInfo(listitem) + listitem = PlaybackUtils(xml[0], self.mgr).setArtwork(listitem) + result.listitem = listitem + else: + # Video and Music + with lock: + result = PlaybackUtils(xml[0], self.mgr).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 ##===----") diff --git a/resources/lib/playbackutils.py b/resources/lib/playbackutils.py index b35b9001..b146624d 100644 --- a/resources/lib/playbackutils.py +++ b/resources/lib/playbackutils.py @@ -3,21 +3,25 @@ ############################################################################### import logging -import sys from urllib import urlencode +from threading import Thread -import xbmc +from xbmc import getCondVisibility, Player import xbmcgui -import xbmcplugin import playutils as putils -from playqueue import Playqueue from utils import window, settings, tryEncode, tryDecode import downloadutils -import PlexAPI -import PlexFunctions as PF -import playlist_func as PL +from PlexAPI import API +from PlexFunctions import GetPlexPlaylist, KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE, \ + KODITYPE_FROM_PLEXTYPE +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 playqueue import lock +from pickler import Playback_Successful ############################################################################### @@ -30,47 +34,47 @@ addonName = "PlexKodiConnect" class PlaybackUtils(): - def __init__(self, item): - + def __init__(self, item, callback): + self.mgr = callback self.item = item - self.API = PlexAPI.API(item) - - self.userid = window('currUserId') - self.server = window('pms_server') - self.pl = Playqueue().get_playqueue_from_type( - PF.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[item[0].attrib.get('type')]) - - def play(self, itemid, dbid=None): + self.api = API(item) + self.playqueue = self.mgr.playqueue.get_playqueue_from_type( + KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[self.api.getType()]) + def play(self, plex_id, kodi_id=None, plex_lib_UUID=None): + """ + plex_lib_UUID: xml attribute 'librarySectionUUID', needed for posting + to the PMS + """ + log.info("Playbackutils called") 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.") - log.debug('Playqueue: %s' % self.pl) + api = self.api + playqueue = self.playqueue + xml = None + result = Playback_Successful() + listitem = ListItem() + playutils = putils.PlayUtils(item) playurl = playutils.getPlayUrl() 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'): - # Item is not in Kodi database, is a trailer or plex redirect + if kodi_id in (None, 'plextrailer', 'plexnode'): + # Item is not in Kodi database, is a trailer/clip or plex redirect # e.g. plex.tv watch later - API.CreateListItemFromPlexItem(listitem) + api.CreateListItemFromPlexItem(listitem) self.setArtwork(listitem) - if dbid == 'plexnode': + if kodi_id == 'plexnode': # Need to get yet another xml to get final url window('emby_%s.playmethod' % playurl, clear=True) xml = downloadutils.DownloadUtils().downloadUrl( - '{server}%s' % item[0][0][0].attrib.get('key')) - if xml in (None, 401): + '{server}%s' % item[0][0].attrib.get('key')) + try: + xml[0].attrib + except (TypeError, AttributeError): log.error('Could not download %s' - % item[0][0][0].attrib.get('key')) - return xbmcplugin.setResolvedUrl( - int(sys.argv[1]), False, listitem) + % item[0][0].attrib.get('key')) + return playurl = tryEncode(xml[0].attrib.get('key')) window('emby_%s.playmethod' % playurl, value='DirectStream') @@ -82,20 +86,23 @@ class PlaybackUtils(): window('emby_%s.playmethod' % playurl, "Transcode") listitem.setPath(playurl) self.setProperties(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' window('plex_contextplay', clear=True) - homeScreen = xbmc.getCondVisibility('Window.IsActive(home)') - kodiPl = self.pl.kodi_pl - sizePlaylist = kodiPl.size() + homeScreen = getCondVisibility('Window.IsActive(home)') + sizePlaylist = len(playqueue.items) if contextmenu_play: # Need to start with the items we're inserting here startPos = sizePlaylist else: # Can return -1 - startPos = max(kodiPl.getposition(), 0) + startPos = max(playqueue.kodi_pl.getposition(), 0) self.currentPosition = startPos propertiesPlayback = window('plex_playbackProps') == "true" @@ -107,8 +114,8 @@ class PlaybackUtils(): log.info("Playlist plugin position: %s" % self.currentPosition) log.info("Playlist size: %s" % sizePlaylist) - ############### RESUME POINT ################ - seektime, runtime = API.getRuntime() + # RESUME POINT ################ + seektime, runtime = api.getRuntime() if window('plex_customplaylist.seektime'): # Already got seektime, e.g. from playqueue & Plex companion seektime = int(window('plex_customplaylist.seektime')) @@ -116,64 +123,69 @@ class PlaybackUtils(): # We need to ensure we add the intro and additional parts only once. # Otherwise we get a loop. if not propertiesPlayback: - window('plex_playbackProps', value="true") log.info("Setting up properties in playlist.") - - # Post playQueue to PMS + # Where will the player need to start? + # Do we need to get trailers? trailers = False - if settings('enableCinema') == "true": + if (api.getType() == 'movie' and not seektime and + settings('enableCinema') == "true"): if settings('askCinema') == "true": - trailers = xbmcgui.Dialog().yesno(addonName, - "Play trailers?") + trailers = xbmcgui.Dialog().yesno( + addonName, + "Play trailers?") else: trailers = True - xml = PF.GetPlexPlaylist( - itemid, - item.attrib.get('librarySectionUUID'), - mediatype=API.getType(), + # Post to the PMS. REUSE THE PLAYQUEUE! + xml = GetPlexPlaylist( + plex_id, + plex_lib_UUID, + mediatype=api.getType(), trailers=trailers) - # Save playQueueID for other PKC python instance & kodimonitor - window('plex_playQueueID', value=xml.attrib.get('playQueueID')) + log.debug('xml: ID: %s' % xml.attrib['playQueueID']) + get_playlist_details_from_xml(playqueue, xml=xml) + log.debug('finished ') if (not homeScreen and not seektime and window('plex_customplaylist') != "true" and not contextmenu_play): + # Need to add a dummy file because the first item will fail log.debug("Adding dummy file to playlist.") - # Make sure Kodimonitor recognizes dummy - listitem.setLabel('plex_dummyfile') dummyPlaylist = True - PL.add_listitem_to_Kodi_playlist( - self.pl, - listitem, + add_listitem_to_Kodi_playlist( + playqueue, + startPos, + xbmcgui.ListItem(), playurl, - startPos) + xml[0]) # Remove the original item from playlist - PL.remove_from_Kodi_playlist(self.pl, startPos+1) + remove_from_Kodi_playlist( + playqueue, + startPos+1) # Readd the original item to playlist - via jsonrpc so we have # full metadata - PL.insert_into_Kodi_playlist( - self.pl, + add_item_to_kodi_playlist( + playqueue, self.currentPosition+1, - dbid, - PF.KODITYPE_FROM_PLEXTYPE[API.getType()]) + kodi_id=kodi_id, + kodi_type=kodi_type, + file=playurl) self.currentPosition += 1 - ############### -- CHECK FOR INTROS ################ - if trailers and not seektime: - # if we have any play them when the movie/show is not being resumed + # -- ADD TRAILERS ################ + if trailers: introsPlaylist = self.AddTrailers(xml) - ############### -- ADD MAIN ITEM ONLY FOR HOMESCREEN ############## - + # -- ADD MAIN ITEM ONLY FOR HOMESCREEN ############## if homeScreen and not seektime and not sizePlaylist: # Extend our current playlist with the actual item to play # only if there's no playlist first log.info("Adding main item to playlist.") - PL.add_dbid_to_Kodi_playlist( - self.pl, - dbid=dbid, - mediatype=PF.KODITYPE_FROM_PLEXTYPE[API.getType()]) + add_item_to_kodi_playlist( + playqueue, + self.currentPosition, + kodi_id, + kodi_type) elif contextmenu_play: if window('useDirectPaths') == 'true': @@ -187,17 +199,16 @@ class PlaybackUtils(): listitem, tryDecode(playurl))) window('emby_%s.playmethod' % playurl, value="Transcode") + api.CreateListItemFromPlexItem(listitem) self.setProperties(playurl, listitem) self.setArtwork(listitem) - API.CreateListItemFromPlexItem(listitem) kodiPl.add(playurl, listitem, index=self.currentPosition+1) else: # Full metadata - PL.insert_into_Kodi_playlist( - self.pl, + self.pl.insertintoPlaylist( self.currentPosition+1, - dbid=dbid, - mediatype=PF.KODITYPE_FROM_PLEXTYPE[API.getType()]) + kodi_id, + kodi_type) self.currentPosition += 1 if seektime: window('plex_customplaylist.seektime', value=str(seektime)) @@ -205,44 +216,50 @@ class PlaybackUtils(): # Ensure that additional parts are played after the main item self.currentPosition += 1 - ############### -- CHECK FOR ADDITIONAL PARTS ################ - if len(item[0][0]) > 1: + # -- CHECK FOR ADDITIONAL PARTS ################ + if len(item[0]) > 1: # Only add to the playlist after intros have played - for counter, part in enumerate(item[0][0]): + 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) + api.setPartNumber(counter) additionalListItem = xbmcgui.ListItem() additionalPlayurl = playutils.getPlayUrl( partNumber=counter) log.debug("Adding additional part: %s, url: %s" % (counter, additionalPlayurl)) - + api.CreateListItemFromPlexItem(additionalListItem) self.setProperties(additionalPlayurl, additionalListItem) self.setArtwork(additionalListItem) - # NEW to Plex - API.CreateListItemFromPlexItem(additionalListItem) - - kodiPl.add(additionalPlayurl, additionalListItem, - index=self.currentPosition) + add_listitem_to_playlist( + playqueue, + self.currentPosition, + additionalListItem, + kodi_id=kodi_id, + kodi_type=kodi_type, + plex_id=plex_id, + file=additionalPlayurl) self.currentPosition += 1 - API.setPartNumber(0) + api.setPartNumber(0) if dummyPlaylist: # Added a dummy file to the playlist, # because the first item is going to fail automatically. 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! + with lock: + del playqueue.items[startPos] + # Don't attach listitem + return result # We just skipped adding properties. Reset flag for next time. elif propertiesPlayback: log.debug("Resetting properties playback flag.") window('plex_playbackProps', clear=True) - #self.pl.verifyPlaylist() - ########## SETUP MAIN ITEM ########## + # SETUP MAIN ITEM ########## # For transcoding only, ask for audio/subs pref if (window('emby_%s.playmethod' % playurl) == "Transcode" and not contextmenu_play): @@ -254,40 +271,42 @@ class PlaybackUtils(): listitem.setPath(playurl) self.setProperties(playurl, listitem) - ############### PLAYBACK ################ + # PLAYBACK ################ if (homeScreen and seektime and window('plex_customplaylist') != "true" and not contextmenu_play): - log.info("Play as a widget item.") - API.CreateListItemFromPlexItem(listitem) - xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem) + log.info("Play as a widget item") + api.CreateListItemFromPlexItem(listitem) + result.listitem = listitem + return result elif ((introsPlaylist and window('plex_customplaylist') == "true") or (homeScreen and not sizePlaylist) or contextmenu_play): # Playlist was created just now, play it. # Contextmenu plays always need this - log.info("Play playlist.") - xbmcplugin.endOfDirectory(int(sys.argv[1]), True, False, False) - xbmc.Player().play(kodiPl, startpos=startPos) - + log.info("Play playlist") + # Need a separate thread because Player won't return in time + thread = Thread(target=Player().play, + args=(playqueue.kodi_pl, None, False, startPos)) + thread.setDaemon(True) + thread.start() + # Don't attach listitem + return result else: - log.info("Play as a regular item.") - xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem) + log.info("Play as a regular item") + result.listitem = listitem + return result def AddTrailers(self, xml): """ Adds trailers to a movie, if applicable. Returns True if trailers were added """ - # Failure when downloading trailer playQueue - if xml in (None, 401): - return False # Failure when getting trailers, e.g. when no plex pass if xml.attrib.get('size') == '1': return False - # Playurl needs to point back so we can get metadata! - path = "plugin://plugin.video.plexkodiconnect.movies/" + path = "plugin://plugin.video.plexkodiconnect/movies/" params = { 'mode': "play", 'dbid': 'plextrailer' @@ -296,28 +315,29 @@ class PlaybackUtils(): # Don't process the last item - it's the original movie if counter == len(xml)-1: break - # The server randomly returns intros, process them. - # introListItem = xbmcgui.ListItem() - # introPlayurl = putils.PlayUtils(intro).getPlayUrl() - introAPI = PlexAPI.API(intro) + introAPI = API(intro) + listitem = introAPI.CreateListItemFromPlexItem() params['id'] = introAPI.getRatingKey() params['filename'] = introAPI.getKey() introPlayurl = path + '?' + urlencode(params) + self.setArtwork(listitem, introAPI) + # Overwrite the Plex url + listitem.setPath(introPlayurl) log.info("Adding Intro: %s" % introPlayurl) - - PL.insert_into_Kodi_playlist( - self.pl, + add_listitem_to_Kodi_playlist( + self.playqueue, self.currentPosition, - url=introPlayurl) + listitem, + introPlayurl, + intro) self.currentPosition += 1 - 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() + itemid = self.api.getRatingKey() + itemtype = self.api.getType() + userdata = self.api.getUserData() embyitem = "emby_%s" % playurl window('%s.runtime' % embyitem, value=str(userdata['Runtime'])) @@ -327,20 +347,22 @@ class PlaybackUtils(): if itemtype == "episode": window('%s.refreshid' % embyitem, - value=self.API.getParentRatingKey()) + 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) + subtitles = self.api.externalSubs(playurl) listitem.setSubtitles(subtitles) self.setArtwork(listitem) - def setArtwork(self, listItem): - allartwork = self.API.getAllArtwork(parentInfo=True) + def setArtwork(self, listItem, api=None): + if api is None: + api = self.api + allartwork = api.getAllArtwork(parentInfo=True) arttypes = { 'poster': "Primary", 'tvshow.poster': "Thumb", diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index 9e26f631..5bc64dbb 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -3,7 +3,7 @@ from urllib import quote import embydb_functions as embydb from downloadutils import DownloadUtils as DU -from utils import window, JSONRPC, tryEncode +from utils import JSONRPC, tryEncode, tryDecode from PlexAPI import API ############################################################################### @@ -12,6 +12,9 @@ log = logging.getLogger("PLEX."+__name__) ############################################################################### +# kodi_item: +# {u'type': u'movie', u'id': 3, 'file': path-to-file} + class Playlist_Object_Baseclase(object): playlistid = None # Kodi playlist ID, [int] @@ -25,8 +28,6 @@ class Playlist_Object_Baseclase(object): selectedItemOffset = None shuffled = 0 # [int], 0: not shuffled, 1: ??? 2: ??? repeat = 0 # [int], 0: not repeated, 1: ??? 2: ??? - # Hack to later ignore all Kodi playlist adds that PKC did (Kodimonitor) - PKC_playlist_edits = [] def __repr__(self): answ = "<%s: " % (self.__class__.__name__) @@ -52,7 +53,6 @@ class Playlist_Object_Baseclase(object): self.selectedItemOffset = None self.shuffled = 0 self.repeat = 0 - self.PKC_playlist_edits = [] log.debug('Playlist cleared: %s' % self) def log_Kodi_playlist(self): @@ -84,7 +84,7 @@ class Playlist_Item(object): return answ[:-2] + ">" -def playlist_item_from_kodi_item(kodi_item): +def playlist_item_from_kodi(kodi_item): """ Turns the JSON answer from Kodi into a playlist element @@ -99,14 +99,15 @@ def playlist_item_from_kodi_item(kodi_item): kodi_item['type']) try: item.plex_id = emby_dbitem[0] - item.plex_UUID = emby_dbitem[0] + item.plex_UUID = emby_dbitem[0] # we dont need the uuid yet :-) except TypeError: pass - item.file = kodi_item.get('file') if kodi_item.get('file') else None - item.kodi_type = kodi_item.get('type') if kodi_item.get('type') else None + 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 @@ -115,6 +116,8 @@ def playlist_item_from_kodi_item(kodi_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 @@ -128,6 +131,26 @@ def playlist_item_from_plex(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 embydb.GetEmbyDB() as emby_db: + db_element = emby_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 @@ -154,7 +177,7 @@ def _get_playListVersion_from_xml(playlist, xml): return True -def _get_playlist_details_from_xml(playlist, xml): +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 @@ -174,16 +197,42 @@ def _get_playlist_details_from_xml(playlist, xml): 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 download 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): """ - Supply either with a plex_id OR the data supplied by Kodi JSON-RPC + 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_item(kodi_item) + item = playlist_item_from_kodi(kodi_item) params = { 'next': 0, 'type': playlist.type, @@ -192,22 +241,74 @@ def init_Plex_playlist(playlist, plex_id=None, kodi_item=None): xml = DU().downloadUrl(url="{server}/%ss" % playlist.kind, action_type="POST", parameters=params) - _get_playlist_details_from_xml(playlist, xml) + get_playlist_details_from_xml(playlist, xml) playlist.items.append(item) - log.debug('Initialized the playlist: %s' % playlist) + log.debug('Initialized the playlist on the Plex side: %s' % playlist) -def add_playlist_item(playlist, kodi_item, after_pos): +def add_listitem_to_playlist(playlist, pos, listitem, kodi_id=None, + kodi_type=None, plex_id=None, file=None): """ - Adds the new kodi_item to playlist after item at position after_pos - [int] + Adds a listitem to both the Kodi and Plex playlist at position pos [int]. + + If file is not None, file will overrule kodi_id! """ - item = playlist_item_from_kodi_item(kodi_item) + 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 playlist + # Will always put the new item at the end of the Plex playlist xml = DU().downloadUrl(url, action_type="PUT") try: - item.ID = xml.attrib['%sLastAddedItemID' % playlist.kind] + item.ID = xml[-1].attrib['%sItemID' % playlist.kind] except (TypeError, AttributeError, KeyError): log.error('Could not add item %s to playlist %s' % (kodi_item, playlist)) @@ -218,21 +319,48 @@ def add_playlist_item(playlist, kodi_item, after_pos): if plex_item.attrib['%sItemID' % playlist.kind] == item.ID: item.guid = plex_item.attrib['guid'] playlist.items.append(item) - if after_pos == len(playlist.items) - 1: + 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, - after_pos) + 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] + 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' % (before_pos, after_pos)) + 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, @@ -249,6 +377,7 @@ def move_playlist_item(playlist, before_pos, after_pos): _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): @@ -280,32 +409,14 @@ def refresh_playlist_from_PMS(playlist): except: log.error('Could not download Plex playlist.') return - _get_playlist_details_from_xml(playlist, xml) + get_playlist_details_from_xml(playlist, xml) -def update_playlist_from_PMS(playlist, playlist_id=None): +def delete_playlist_item_from_PMS(playlist, pos): """ - Updates Kodi playlist using a new PMS playlist. Pass in playlist_id if we - need to fetch a new playqueue - """ - xml = get_PMS_playlist(playlist, playlist_id) - try: - xml.attrib['%sVersion' % playlist.kind] - except: - log.error('Could not download 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 delete_playlist_item(playlist, pos): - """ - Delete the item at position pos [int] + 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, @@ -313,7 +424,7 @@ def delete_playlist_item(playlist, pos): playlist.repeat), action_type="DELETE") _get_playListVersion_from_xml(playlist, xml) - del playlist.items[pos], playlist.old_kodi_pl[pos] + del playlist.items[pos] def get_kodi_playlist_items(playlist): @@ -357,78 +468,53 @@ 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). - Will return a Playlist_Item + Returns a Playlist_Item """ - item = Playlist_Item() - api = API(xml_video_element) + item = playlist_item_from_xml(playlist, xml_video_element) params = { 'playlistid': playlist.playlistid } - 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 embydb.GetEmbyDB() as emby_db: - db_element = emby_db.getItem_byId(item.plex_id) - try: - item.kodi_id, item.kodi_type = int(db_element[0]), db_element[4] - except TypeError: - pass if item.kodi_id: params['item'] = {'%sid' % item.kodi_type: item.kodi_id} else: - item.file = api.getFilePath() params['item'] = {'file': tryEncode(item.file)} log.debug(JSONRPC('Playlist.Add').execute(params)) - playlist.PKC_playlist_edits.append( - item.kodi_id if item.kodi_id else item.file) return item -def add_listitem_to_Kodi_playlist(playlist, listitem, file, index): +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. Will be ignored by kodimonitor - by settings window('plex_ignore_Playlist.OnAdd') + Adds an xbmc listitem to the Kodi playlist.xml_video_element + + WILL NOT UPDATE THE PLEX SIDE, BUT WILL UPDATE OUR PLAYLISTS """ - playlist.kodi_pl.add(file, listitem, index=index) - - -def add_dbid_to_Kodi_playlist(playlist, dbid=None, mediatype=None, url=None): - params = { - 'playlistid': playlist.playlistid - } - if dbid is not None: - params['item'] = {'%sid' % tryEncode(mediatype): int(dbid)} + 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: - params['item'] = {'file': url} - log.debug(JSONRPC('Playlist.Add').execute(params)) + 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, position): +def remove_from_Kodi_playlist(playlist, pos): """ - Removes the item at position from the Kodi playlist using JSON. Will be - ignored by kodimonitor by settings window('plex_ignore_Playlist.OnRemove') + 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 playlist %s' % (position, playlist)) + log.debug('Removing position %s from Kodi only from %s' % (pos, playlist)) log.debug(JSONRPC('Playlist.Remove').execute({ 'playlistid': playlist.playlistid, - 'position': position + 'position': pos })) - - -def insert_into_Kodi_playlist(playlist, position, dbid=None, mediatype=None, - url=None): - """ - """ - params = { - 'playlistid': playlist.playlistid, - 'position': position - } - if dbid is not None: - params['item'] = {'%sid' % tryEncode(mediatype): int(dbid)} - else: - params['item'] = {'file': url} - JSONRPC('Playlist.Insert').execute(params) + del playlist.items[pos] # NOT YET UPDATED!! @@ -478,7 +564,7 @@ def _addtoPlaylist_xbmc(self, item): 'id': API.getRatingKey(), 'filename': API.getKey() } - playurl = "plugin://plugin.video.plexkodiconnect.movies/?%s" \ + playurl = "plugin://plugin.video.plexkodiconnect/movies/?%s" \ % urlencode(params) listitem = API.CreateListItemFromPlexItem() diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index 7f589cc8..1d25b886 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -1,21 +1,19 @@ # -*- coding: utf-8 -*- ############################################################################### import logging -from threading import Lock, Thread +from threading import RLock, Thread -import xbmc +from xbmc import sleep, Player, PlayList, PLAYLIST_MUSIC, PLAYLIST_VIDEO -from utils import window, ThreadMethods, ThreadMethodsAdditionalSuspend, \ - Lock_Function +from utils import window, ThreadMethods, ThreadMethodsAdditionalSuspend import playlist_func as PL from PlexFunctions import ConvertPlexToKodiTime ############################################################################### log = logging.getLogger("PLEX."+__name__) -# Lock used to lock methods -lock = Lock() -lockmethod = Lock_Function(lock) +# Lock used for playqueue manipulations +lock = RLock() ############################################################################### @@ -29,33 +27,32 @@ class Playqueue(Thread): __shared_state = {} playqueues = None - @lockmethod.lockthis def __init__(self, callback=None): self.__dict__ = self.__shared_state Thread.__init__(self) if self.playqueues is not None: return self.mgr = callback - self.player = xbmc.Player() # Initialize Kodi playqueues - 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 = xbmc.PlayList(xbmc.PLAYLIST_MUSIC) - elif playqueue.type == 'video': - playqueue.kodi_pl = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) - else: - # Currently, only video or audio playqueues available - playqueue.kodi_pl = xbmc.PlayList(xbmc.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) + 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) def get_playqueue_from_type(self, typus): @@ -63,30 +60,14 @@ class Playqueue(Thread): Returns the playqueue according to the typus ('video', 'audio', 'picture') passed in """ - for playqueue in self.playqueues: - if playqueue.type == typus: - break - else: - raise ValueError('Wrong type was passed in: %s' % typus) - return playqueue + 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 get_playqueue_from_playerid(self, kodi_player_id): - for playqueue in self.playqueues: - if playqueue.playlistid == kodi_player_id: - break - else: - raise ValueError('Wrong kodi_player_id passed was passed in: %s' - % kodi_player_id) - return playqueue - - def _grab_PMS_playqueue(self, playqueue, playqueue_id=None, repeat=None): - """ - For initiating out playqueues from the PMS because another PKC Python - instance already is setting up the Kodi playlists - """ - PL.grab_PMS_playqueue(playqueue, playqueue_id) - - @lockmethod.lockthis def update_playqueue_from_PMS(self, playqueue, playqueue_id=None, @@ -97,169 +78,104 @@ class Playqueue(Thread): in playqueue_id if we need to fetch a new playqueue repeat = 0, 1, 2 - offset = time offset in Plextime + offset = time offset in Plextime (milliseconds) """ - log.info('New playqueue received from the PMS, updating!') - PL.update_playlist_from_PMS(playqueue, playqueue_id) - playqueue.repeat = 0 if not repeat else int(repeat) - log.debug('Updated playqueue: %s' % playqueue) - 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 = None - # Start playback - if startpos: - self.player.play(playqueue.kodi_pl, startpos=startpos) - else: - self.player.play(playqueue.kodi_pl) - playqueue.log_Kodi_playlist() + log.info('New playqueue %s received from Plex companion with offset ' + '%s, repeat %s' % (playqueue_id, offset, repeat)) + with lock: + if playqueue_id != playqueue.ID: + log.debug('Need to fetch new playQueue from the PMS') + PL.update_playlist_from_PMS(playqueue, playqueue_id) + else: + log.debug('Restarting existing playQueue') + PL.refresh_playlist_from_PMS(playqueue) + 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 = None + # Start playback. Player does not return in time + if startpos: + thread = Thread(target=Player().play, + args=(playqueue.kodi_pl, + None, + False, + startpos)) + else: + thread = Thread(target=Player().play, + args=(playqueue.kodi_pl,)) + thread.setDaemon(True) + thread.start() - @lockmethod.lockthis - def start_playqueue_initiated_by_companion(self, - playqueue, - playqueue_id=None, - repeat=None, - offset=None): - log.info('Plex companion wants to restart playback of playqueue %s' - % playqueue) - # Still need to get new playQueue from the server - don't know what has - # been selected - PL.refresh_playlist_from_PMS(playqueue) - 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 = None - # Start playback - if startpos: - self.player.play(playqueue.kodi_pl, startpos=startpos) - else: - self.player.play(playqueue.kodi_pl) - playqueue.log_Kodi_playlist() - - @lockmethod.lockthis - def kodi_onadd(self, data): - """ - Called if an item is added to a Kodi playqueue. Data is Kodi JSON-RPC - output, e.g. - { - u'item': {u'type': u'movie', u'id': 3}, - u'playlistid': 1, - u'position': 0 - } - """ - playqueue = self.playqueues[data['playlistid']] - if window('plex_playbackProps') == 'true': - log.debug('kodi_onadd called during PKC playback setup') - if window('plex_playQueueID'): - self._grab_PMS_playqueue(playqueue, window('plex_playQueueID')) - window('plex_playQueueID', clear=True) - log.debug('Done setting up playQueue') - return - - if playqueue.PKC_playlist_edits: - old = (data['item'].get('id') if data['item'].get('id') - else data['item'].get('file')) - for i, item in enumerate(playqueue.PKC_playlist_edits): - if old == item: - log.debug('kodimonitor told us of a PKC edit - ignore') - del playqueue.PKC_playlist_edits[i] - return - if playqueue.ID is None: - # Need to initialize the queue for the first time - PL.init_Plex_playlist(playqueue, kodi_item=data['item']) - else: - PL.add_playlist_item(playqueue, data['item'], data['position']) - log.debug('Added a new item to the playqueue: %s' % playqueue) - - @lockmethod.lockthis - def kodi_onremove(self, data): - """ - Called if an item is removed from a Kodi playqueue. Data is Kodi JSON- - RPC output, e.g. - {u'position': 2, u'playlistid': 1} - """ - if window('plex_playbackProps') == 'true': - log.debug('kodi_onremove called during PKC playback setup') - return - playqueue = self.playqueues[data['playlistid']] - PL.delete_playlist_item(playqueue, data['position']) - log.debug('Deleted item at position %s. New playqueue: %s' - % (data['position'], playqueue)) - - @lockmethod.lockthis def _compare_playqueues(self, playqueue, new): """ Used to poll the Kodi playqueue and update the Plex playqueue if needed """ - if self.threadStopped(): - # Chances are that we got an empty Kodi playlist due to Kodi exit - return - old = playqueue.old_kodi_pl + old = list(playqueue.items) index = list(range(0, len(old))) log.debug('Comparing new Kodi playqueue %s with our play queue %s' - % (new, playqueue)) + % (new, old)) for i, new_item in enumerate(new): for j, old_item in enumerate(old): - if old_item.get('id') is None: - identical = old_item['file'] == new_item['file'] + 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['id'] == new_item['id'] and - old_item['type'] == new_item['type']) + 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: - # item now at pos i has been moved from original pos i+j + log.debug('Detected playqueue item %s moved to position %s' + % (i+j, i)) PL.move_playlist_item(playqueue, i + j, i) - # Delete the item we just found del old[j], index[j] break - # New elements and left-over elements will be taken care of by the kodi - # monitor! - log.debug('New playqueue: %s' % playqueue) - - def init_playlists(self): - """ - Initializes the playqueues with already existing items. - Called on startup AND for addon paths! - """ - 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: + log.debug('Detected new Kodi element: %s' % new_item) + if playqueue.ID is None: + PL.init_Plex_playlist(playqueue, + kodi_item=new_item) else: - PL.add_playlist_item(playqueue, item, i) + PL.add_item_to_PMS_playlist(playqueue, + i, + kodi_item=new_item) + 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 - self.init_playlists() + 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 - xbmc.sleep(1000) - for playqueue in self.playqueues: - if not playqueue.items: - # Skip empty playqueues as items can't be modified - continue - 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) - xbmc.sleep(1000) + 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 ##===----") diff --git a/resources/lib/utils.py b/resources/lib/utils.py index be6d96d3..eeabdb62 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -56,6 +56,24 @@ def window(property, value=None, clear=False, windowid=10000): 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): """ Get or add addon setting. Returns unicode diff --git a/service.py b/service.py index c5bf737d..fc6053cd 100644 --- a/service.py +++ b/service.py @@ -44,6 +44,8 @@ from playqueue import Playqueue import PlexAPI from PlexCompanion import PlexCompanion +from monitor_kodi_play import Monitor_Kodi_Play +from playback_starter import Playback_Starter ############################################################################### @@ -73,6 +75,7 @@ class Service(): plexCompanion_running = False playqueue_running = False kodimonitor_running = False + playback_starter_running = False def __init__(self): @@ -109,7 +112,8 @@ class Service(): "plex_authenticated", "PlexUserImage", "useDirectPaths", "suspend_LibraryThread", "plex_terminateNow", "kodiplextimeoffset", "countError", "countUnauthorized", - "plex_restricteduser", "plex_allows_mediaDeletion" + "plex_restricteduser", "plex_allows_mediaDeletion", + "plex_play_new_item", "plex_result" ] for prop in properties: window(prop, clear=True) @@ -133,6 +137,10 @@ class Service(): monitor = self.monitor 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 initialsetup.InitialSetup().setup() @@ -142,6 +150,7 @@ class Service(): self.library = LibrarySync(self) self.plexCompanion = PlexCompanion(self) self.playqueue = Playqueue(self) + self.playback_starter = Playback_Starter(self) plx = PlexAPI.PlexAPI() @@ -197,6 +206,9 @@ class Service(): if not self.plexCompanion_running: self.plexCompanion_running = True self.plexCompanion.start() + if not self.playback_starter_running: + self.playback_starter_running = True + self.playback_starter.start() else: if (self.user.currUser is None) and self.warn_auth: # Alert user is not authenticated and suppress future