def __build_item(xml_element): api = API(xml_element) listitem = api.CreateListItemFromPlexItem() resume = api.getResume() if resume: listitem.setProperty('resumetime', str(resume)) if (api.getKey().startswith('/system/services') or api.getKey().startswith('http')): params = { 'mode': 'plex_node', 'key': xml_element.attrib.get('key'), 'offset': xml_element.attrib.get('viewOffset', '0'), } url = "plugin://%s?%s" % (v.ADDON_ID, urlencode(params)) elif api.getType() == v.PLEX_TYPE_PHOTO: url = api.get_picture_path() else: params = { 'mode': 'play', 'plex_id': api.getRatingKey(), 'plex_type': api.getType(), } url = "plugin://%s?%s" % (v.ADDON_ID, urlencode(params)) xbmcplugin.addDirectoryItem(handle=HANDLE, url=url, listitem=listitem)
def play_resume(playqueue, xml, stack): """ If there exists a resume point, Kodi will ask the user whether to continue playback. We thus need to use setResolvedUrl "correctly". Mind that there might be several parts! """ result = Playback_Successful() listitem = PKC_ListItem() # Only get the very first item of our playqueue (i.e. the very first part) stack_item = stack.pop(0) api = API(xml[0]) item = PL.playlist_item_from_xml(playqueue, xml[0], kodi_id=stack_item['kodi_id'], kodi_type=stack_item['kodi_type']) api.setPartNumber(item.part) item.playcount = stack_item['playcount'] item.offset = stack_item['offset'] item.part = stack_item['part'] api.CreateListItemFromPlexItem(listitem) playutils = PlayUtils(api, item) playurl = playutils.getPlayUrl() listitem.setPath(tryEncode(playurl)) if item.playmethod in ('DirectStream', 'DirectPlay'): listitem.setSubtitles(api.externalSubs()) else: playutils.audio_subtitle_prefs(listitem) result.listitem = listitem # Add to our playlist playqueue.items.append(item) # This will release default.py with setResolvedUrl pickle_me(result) # Add remaining parts to the playlist, if any if stack: _process_stack(playqueue, stack)
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 (IndexError, TypeError, AttributeError): log.error('Could not get a PMS xml for plex id %s' % plex_id) return api = API(xml[0]) if api.getType() == v.PLEX_TYPE_PHOTO: # Photo result = Playback_Successful() listitem = PKC_ListItem() listitem = api.CreateListItemFromPlexItem(listitem) result.listitem = listitem else: # Video and Music playqueue = self.playqueue.get_playqueue_from_type( v.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 conclude_playback(playqueue, pos): """ ONLY if actually being played (e.g. at 5th position of a playqueue). Decide on direct play, direct stream, transcoding path to direct paths: file itself PMS URL Web URL audiostream (e.g. let user choose) subtitle stream (e.g. let user choose) Init Kodi Playback (depending on situation): start playback return PKC listitem attached to result """ LOG.info('Concluding playback for playqueue position %s', pos) result = Playback_Successful() listitem = PKC_ListItem() item = playqueue.items[pos] if item.xml is not None: # Got a Plex element api = API(item.xml) api.setPartNumber(item.part) api.CreateListItemFromPlexItem(listitem) playutils = PlayUtils(api, item) playurl = playutils.getPlayUrl() else: playurl = item.file listitem.setPath(tryEncode(playurl)) if item.playmethod in ('DirectStream', 'DirectPlay'): listitem.setSubtitles(api.externalSubs()) else: playutils.audio_subtitle_prefs(listitem) if state.RESUME_PLAYBACK is True: state.RESUME_PLAYBACK = False if (item.offset is None and item.plex_type not in (v.PLEX_TYPE_SONG, v.PLEX_TYPE_CLIP)): with plexdb.Get_Plex_DB() as plex_db: plex_dbitem = plex_db.getItem_byId(item.plex_id) file_id = plex_dbitem[1] if plex_dbitem else None with kodidb.GetKodiDB('video') as kodi_db: item.offset = kodi_db.get_resume(file_id) LOG.info('Resuming playback at %s', item.offset) listitem.setProperty('StartOffset', str(item.offset)) listitem.setProperty('resumetime', str(item.offset)) # Reset the resumable flag state.RESUMABLE = False result.listitem = listitem pickle_me(result) LOG.info('Done concluding playback')
def _prep_playlist_stack(xml): stack = [] for item in xml: api = API(item) if (state.CONTEXT_MENU_PLAY is False and api.getType() != v.PLEX_TYPE_CLIP): # If user chose to play via PMS or force transcode, do not # use the item path stored in the Kodi DB with plexdb.Get_Plex_DB() as plex_db: plex_dbitem = plex_db.getItem_byId(api.getRatingKey()) kodi_id = plex_dbitem[0] if plex_dbitem else None kodi_type = plex_dbitem[4] if plex_dbitem else None else: # We will never store clips (trailers) in the Kodi DB kodi_id = None kodi_type = None for part, _ in enumerate(item[0]): api.setPartNumber(part) if kodi_id is None: # Need to redirect again to PKC to conclude playback params = { 'mode': 'play', 'plex_id': api.getRatingKey(), 'plex_type': api.getType() } path = ('plugin://plugin.video.plexkodiconnect?%s' % urlencode(params)) listitem = api.CreateListItemFromPlexItem() listitem.setPath(tryEncode(path)) else: # Will add directly via the Kodi DB path = None listitem = None stack.append({ 'kodi_id': kodi_id, 'kodi_type': kodi_type, 'file': path, 'xml_video_element': item, 'listitem': listitem, 'part': part, 'playcount': api.getViewCount(), 'offset': api.getResume(), 'id': api.getItemId() }) return stack
def add_trailer(self, item): # Playurl needs to point back so we can get metadata! path = "plugin://plugin.video.plexkodiconnect/movies/" params = {'mode': "play", 'dbid': 'plextrailer'} introAPI = API(item) listitem = introAPI.CreateListItemFromPlexItem() params['id'] = introAPI.getRatingKey() params['filename'] = introAPI.getKey() introPlayurl = path + '?' + urlencode(params) introAPI.set_listitem_artwork(listitem) # Overwrite the Plex url listitem.setPath(introPlayurl) log.info("Adding Plex trailer: %s" % introPlayurl) add_listitem_to_Kodi_playlist(self.playqueue, self.currentPosition, listitem, introPlayurl, xml_video_element=item) self.currentPosition += 1
def StartDirectPath(self, plex_id, type, currentFile): """ Set some additional stuff if playback was initiated by Kodi, not PKC """ xml = self.doUtils('{server}/library/metadata/%s' % plex_id) try: xml[0].attrib except: log.error('Did not receive a valid XML for plex_id %s.' % plex_id) return False # Setup stuff, because playback was started by Kodi, not PKC api = API(xml[0]) listitem = api.CreateListItemFromPlexItem() api.set_playback_win_props(currentFile, listitem) if type == "song" and settings('streamMusic') == "true": window('plex_%s.playmethod' % currentFile, value="DirectStream") else: window('plex_%s.playmethod' % currentFile, value="DirectPlay") log.debug('Window properties set for direct paths!')
def watchlater(): """ Listing for plex.tv Watch Later section (if signed in to plex.tv) """ if window('plex_token') == '': log.error('No watch later - not signed in to plex.tv') return xbmcplugin.endOfDirectory(HANDLE, False) if window('plex_restricteduser') == 'true': log.error('No watch later - restricted user') return xbmcplugin.endOfDirectory(HANDLE, False) xml = downloadutils.DownloadUtils().downloadUrl( 'https://plex.tv/pms/playlists/queue/all', authenticate=False, headerOptions={'X-Plex-Token': window('plex_token')}) if xml in (None, 401): log.error('Could not download watch later list from plex.tv') return xbmcplugin.endOfDirectory(HANDLE, False) log.info('Displaying watch later plex.tv items') xbmcplugin.setContent(HANDLE, 'movies') url = "plugin://plugin.video.plexkodiconnect/" params = { 'mode': "Plex_Node", } for item in xml: api = API(item) listitem = api.CreateListItemFromPlexItem() api.AddStreamInfo(listitem) api.set_listitem_artwork(listitem) params['id'] = item.attrib.get('key') params['viewOffset'] = item.attrib.get('viewOffset', '0') params['plex_type'] = item.attrib.get('type') xbmcplugin.addDirectoryItem(handle=HANDLE, url="%s?%s" % (url, urlencode(params)), listitem=listitem) xbmcplugin.endOfDirectory( handle=HANDLE, cacheToDisc=settings('enableTextureCache') == 'true')
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.xml[0] api = API(item) playqueue = self.playqueue xml = None result = Playback_Successful() listitem = ListItem() playutils = putils.PlayUtils(item) playurl = playutils.getPlayUrl() if not playurl: log.error('No playurl found, aborting') return 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.set_listitem_artwork(listitem) if kodi_id == 'plexnode': # Need to get yet another xml to get final url window('plex_%s.playmethod' % playurl, clear=True) xml = downloadutils.DownloadUtils().downloadUrl( '{server}%s' % item[0][0].attrib.get('key')) try: xml[0].attrib except (TypeError, AttributeError): log.error('Could not download %s' % item[0][0].attrib.get('key')) return playurl = tryEncode(xml[0].attrib.get('key')) window('plex_%s.playmethod' % playurl, value='DirectStream') playmethod = window('plex_%s.playmethod' % playurl) if playmethod == "Transcode": playutils.audioSubsPref(listitem, tryDecode(playurl)) listitem.setPath(playurl) api.set_playback_win_props(playurl, listitem) result.listitem = listitem return result kodi_type = v.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 = 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(playqueue.kodi_pl.getposition(), 0) self.currentPosition = startPos propertiesPlayback = window('plex_playbackProps') == "true" introsPlaylist = False dummyPlaylist = False log.info("Playing from contextmenu: %s" % contextmenu_play) log.info("Playlist start position: %s" % startPos) log.info("Playlist plugin position: %s" % self.currentPosition) log.info("Playlist size: %s" % sizePlaylist) # 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')) # 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.") # Where will the player need to start? # Do we need to get trailers? trailers = False if (api.getType() == v.PLEX_TYPE_MOVIE and not seektime and sizePlaylist < 2 and settings('enableCinema') == "true"): if settings('askCinema') == "true": trailers = xbmcgui.Dialog().yesno(lang(29999), "Play trailers?") else: trailers = True # Post to the PMS. REUSE THE PLAYQUEUE! xml = init_plex_playqueue(plex_id, plex_lib_UUID, mediatype=api.getType(), trailers=trailers) try: get_playlist_details_from_xml(playqueue, xml=xml) except KeyError: return if (not homeScreen and not seektime and sizePlaylist < 2 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.") dummyPlaylist = True add_listitem_to_Kodi_playlist(playqueue, startPos, xbmcgui.ListItem(), playurl, xml[0]) # Remove the original item from playlist remove_from_Kodi_playlist(playqueue, startPos + 1) # Readd the original item to playlist - via jsonrpc so we have # full metadata add_item_to_kodi_playlist(playqueue, self.currentPosition + 1, kodi_id=kodi_id, kodi_type=kodi_type, file=playurl) self.currentPosition += 1 # -- ADD TRAILERS ################ if trailers: for i, item in enumerate(xml): if i == len(xml) - 1: # Don't add the main movie itself break self.add_trailer(item) introsPlaylist = True # -- 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.") add_item_to_kodi_playlist(playqueue, self.currentPosition, kodi_id, kodi_type) elif contextmenu_play: if state.DIRECT_PATHS: # Cannot add via JSON with full metadata because then we # Would be using the direct path log.debug("Adding contextmenu item for direct paths") if window('plex_%s.playmethod' % playurl) == "Transcode": playutils.audioSubsPref(listitem, tryDecode(playurl)) api.CreateListItemFromPlexItem(listitem) api.set_playback_win_props(playurl, listitem) api.set_listitem_artwork(listitem) add_listitem_to_Kodi_playlist( playqueue, self.currentPosition + 1, convert_PKC_to_listitem(listitem), file=playurl, kodi_item={ 'id': kodi_id, 'type': kodi_type }) else: # Full metadata$ add_item_to_kodi_playlist(playqueue, self.currentPosition + 1, kodi_id, kodi_type) self.currentPosition += 1 if seektime: window('plex_customplaylist.seektime', value=str(seektime)) # Ensure that additional parts are played after the main item self.currentPosition += 1 # -- CHECK FOR ADDITIONAL PARTS ################ if len(item[0]) > 1: self.add_part(item, api, kodi_id, kodi_type) 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.") # 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. elif propertiesPlayback: log.debug("Resetting properties playback flag.") window('plex_playbackProps', clear=True) # SETUP MAIN ITEM ########## # For transcoding only, ask for audio/subs pref if (window('plex_%s.playmethod' % playurl) == "Transcode" and not contextmenu_play): playutils.audioSubsPref(listitem, tryDecode(playurl)) listitem.setPath(playurl) api.set_playback_win_props(playurl, listitem) api.set_listitem_artwork(listitem) # PLAYBACK ################ if (homeScreen and seektime and window('plex_customplaylist') != "true" and not contextmenu_play): 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 from starting position %s" % startPos) # 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") result.listitem = listitem return result
def getOnDeck(viewid, mediatype, tagname, limit): """ Retrieves Plex On Deck items, currently only for TV shows Input: viewid: Plex id of the library section, e.g. '1' mediatype: Kodi mediatype, e.g. 'tvshows', 'movies', 'homevideos', 'photos' tagname: Name of the Plex library, e.g. "My Movies" limit: Max. number of items to retrieve, e.g. 50 """ xbmcplugin.setContent(HANDLE, 'episodes') appendShowTitle = settings('OnDeckTvAppendShow') == 'true' appendSxxExx = settings('OnDeckTvAppendSeason') == 'true' directpaths = settings('useDirectPaths') == 'true' if settings('OnDeckTVextended') == 'false': # Chances are that this view is used on Kodi startup # Wait till we've connected to a PMS. At most 30s counter = 0 while window('plex_authenticated') != 'true': counter += 1 if counter >= 300: log.error('Aborting On Deck view, we were not authenticated ' 'for the PMS') return xbmcplugin.endOfDirectory(HANDLE, False) sleep(100) xml = downloadutils.DownloadUtils().downloadUrl( '{server}/library/sections/%s/onDeck' % viewid) if xml in (None, 401): log.error('Could not download PMS xml for view %s' % viewid) return xbmcplugin.endOfDirectory(HANDLE) limitcounter = 0 for item in xml: api = API(item) listitem = api.CreateListItemFromPlexItem( appendShowTitle=appendShowTitle, appendSxxExx=appendSxxExx) if directpaths: url = api.getFilePath() else: params = { 'mode': "play", 'plex_id': api.getRatingKey(), 'plex_type': api.getType() } url = "plugin://plugin.video.plexkodiconnect/tvshows/?%s" \ % urlencode(params) xbmcplugin.addDirectoryItem( handle=HANDLE, url=url, listitem=listitem) limitcounter += 1 if limitcounter == limit: break return xbmcplugin.endOfDirectory( handle=HANDLE, cacheToDisc=settings('enableTextureCache') == 'true') # if the addon is called with nextup parameter, # we return the nextepisodes list of the given tagname # First we get a list of all the TV shows - filtered by tag params = { 'sort': {'order': "descending", 'method': "lastplayed"}, 'filter': { 'and': [ {'operator': "true", 'field': "inprogress", 'value': ""}, {'operator': "is", 'field': "tag", 'value': "%s" % tagname} ]} } items = js.get_tv_shows(params) if not items: # Now items retrieved - empty directory xbmcplugin.endOfDirectory(handle=HANDLE) return params = { 'sort': {'method': "episode"}, 'limits': {"end": 1}, 'properties': [ "title", "playcount", "season", "episode", "showtitle", "plot", "file", "rating", "resume", "tvshowid", "art", "streamdetails", "firstaired", "runtime", "cast", "writer", "dateadded", "lastplayed" ], } if settings('ignoreSpecialsNextEpisodes') == "true": params['filter'] = { 'and': [ {'operator': "lessthan", 'field': "playcount", 'value': "1"}, {'operator': "greaterthan", 'field': "season", 'value': "0"} ] } else: params['filter'] = { 'or': [ {'operator': "lessthan", 'field': "playcount", 'value': "1"}, {'operator': "true", 'field': "inprogress", 'value': ""} ] } # Are there any episodes still in progress/not yet finished watching?!? # Then we should show this episode, NOT the "next up" inprog_params = { 'sort': {'method': "episode"}, 'filter': {'operator': "true", 'field': "inprogress", 'value': ""}, 'properties': params['properties'] } count = 0 for item in items: inprog_params['tvshowid'] = item['tvshowid'] episodes = js.get_episodes(inprog_params) if not episodes: # No, there are no episodes not yet finished. Get "next up" params['tvshowid'] = item['tvshowid'] episodes = js.get_episodes(params) if not episodes: # Also no episodes currently coming up continue for episode in episodes: # There will always be only 1 episode ('limit=1') listitem = createListItem(episode, appendShowTitle=appendShowTitle, appendSxxExx=appendSxxExx) xbmcplugin.addDirectoryItem(handle=HANDLE, url=episode['file'], listitem=listitem, isFolder=False) count += 1 if count >= limit: break xbmcplugin.endOfDirectory(handle=HANDLE)
def process_indirect(key, offset, resolve=True): """ Called e.g. for Plex "Play later" - Plex items where we need to fetch an additional xml for the actual playurl. In the PMS metadata, indirect="1" is set. Will release default.py with setResolvedUrl Set resolve to False if playback should be kicked off directly, not via setResolvedUrl """ LOG.info('process_indirect called with key: %s, offset: %s', key, offset) result = Playback_Successful() if key.startswith('http') or key.startswith('{server}'): xml = DU().downloadUrl(key) elif key.startswith('/system/services'): xml = DU().downloadUrl('http://node.plexapp.com:32400%s' % key) else: xml = DU().downloadUrl('{server}%s' % key) try: xml[0].attrib except (TypeError, IndexError, AttributeError): LOG.error('Could not download PMS metadata') if resolve is True: # Release default.py pickle_me(result) return if offset != '0': offset = int(v.PLEX_TO_KODI_TIMEFACTOR * float(offset)) # Todo: implement offset api = API(xml[0]) listitem = PKC_ListItem() api.CreateListItemFromPlexItem(listitem) playqueue = PQ.get_playqueue_from_type( v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.getType()]) playqueue.clear() item = PL.Playlist_Item() item.xml = xml[0] item.offset = int(offset) item.plex_type = v.PLEX_TYPE_CLIP item.playmethod = 'DirectStream' # Need to get yet another xml to get the final playback url xml = DU().downloadUrl('http://node.plexapp.com:32400%s' % xml[0][0][0].attrib['key']) try: xml[0].attrib except (TypeError, IndexError, AttributeError): LOG.error('Could not download last xml for playurl') if resolve is True: # Release default.py pickle_me(result) return playurl = xml[0].attrib['key'] item.file = playurl listitem.setPath(tryEncode(playurl)) playqueue.items.append(item) if resolve is True: result.listitem = listitem pickle_me(result) else: thread = Thread(target=Player().play, args={ 'item': tryEncode(playurl), 'listitem': listitem }) thread.setDaemon(True) LOG.info('Done initializing PKC playback, starting Kodi player') thread.start()
def BrowsePlexContent(viewid, mediatype="", folderid=""): """ Browse Plex Photos: viewid: PMS name of the library mediatype: mediatype, 'photos' nodetype: e.g. 'ondeck' (TBD!!) """ log.debug("BrowsePlexContent called with viewid: %s, mediatype: " "%s, folderid: %s" % (viewid, mediatype, folderid)) if not folderid: # Top-level navigation, so get the content of this section # Get all sections xml = GetPlexSectionResults(viewid, containerSize=int(settings('limitindex'))) try: xml.attrib except AttributeError: log.error("Error download section %s" % viewid) return xbmcplugin.endOfDirectory(HANDLE, False) else: # folderid was passed so we can directly access the folder xml = downloadutils.DownloadUtils().downloadUrl("{server}%s" % folderid) try: xml.attrib except AttributeError: log.error("Error downloading %s" % folderid) return xbmcplugin.endOfDirectory(HANDLE, False) # Set the folder's name xbmcplugin.setPluginCategory(HANDLE, xml.attrib.get('librarySectionTitle')) # set the correct params for the content type if mediatype == "photos": xbmcplugin.setContent(HANDLE, 'photos') # process the listing for item in xml: api = API(item) if item.tag == 'Directory': li = ListItem(item.attrib.get('title', 'Missing title')) # for folders we add an additional browse request, passing the # folderId li.setProperty('IsFolder', 'true') li.setProperty('IsPlayable', 'false') path = "%s?id=%s&mode=browseplex&type=%s&folderid=%s" \ % (ARGV_0, viewid, mediatype, api.getKey()) api.set_listitem_artwork(li) xbmcplugin.addDirectoryItem(handle=HANDLE, url=path, listitem=li, isFolder=True) else: li = api.CreateListItemFromPlexItem() api.set_listitem_artwork(li) xbmcplugin.addDirectoryItem(handle=HANDLE, url=li.getProperty("path"), listitem=li) xbmcplugin.addSortMethod(HANDLE, xbmcplugin.SORT_METHOD_VIDEO_TITLE) xbmcplugin.addSortMethod(HANDLE, xbmcplugin.SORT_METHOD_DATE) xbmcplugin.addSortMethod(HANDLE, xbmcplugin.SORT_METHOD_VIDEO_RATING) xbmcplugin.addSortMethod(HANDLE, xbmcplugin.SORT_METHOD_VIDEO_RUNTIME) xbmcplugin.endOfDirectory( handle=HANDLE, cacheToDisc=settings('enableTextureCache') == 'true')