diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index 3dc6887d..e0c36a16 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -2690,7 +2690,8 @@ class API(): plexitem = "plex_%s" % playurl window('%s.runtime' % plexitem, value=str(userdata['Runtime'])) window('%s.type' % plexitem, value=itemtype) - window('%s.itemid' % plexitem, value=self.getRatingKey()) + state.PLEX_IDS[tryDecode(playurl)] = self.getRatingKey() + # window('%s.itemid' % plexitem, value=self.getRatingKey()) window('%s.playcount' % plexitem, value=str(userdata['PlayCount'])) if itemtype == v.PLEX_TYPE_EPISODE: diff --git a/resources/lib/json_rpc.py b/resources/lib/json_rpc.py index 90dc7af5..0e0dadcb 100644 --- a/resources/lib/json_rpc.py +++ b/resources/lib/json_rpc.py @@ -362,6 +362,7 @@ def get_episodes(params): def get_item(playerid): """ + UNRELIABLE on playback startup! (as other JSON and Python Kodi functions) Returns the following for the currently playing item: { u'title': u'Okja', diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index 6d31faa0..653a019a 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -808,7 +808,7 @@ class Kodidb_Functions(): ids.append(row[0]) return ids - def getIdFromFilename(self, filename, path): + def video_id_from_filename(self, filename, path): """ Returns the tuple (itemId, type) where itemId: Kodi DB unique Id for either movie or episode @@ -884,6 +884,34 @@ class Kodidb_Functions(): return return itemId, typus + def music_id_from_filename(self, filename, path): + """ + Returns the Kodi song_id from the Kodi music database or None if not + found OR something went wrong. + """ + query = ''' + SELECT idPath + FROM path + WHERE strPath = ? + ''' + self.cursor.execute(query, (path,)) + path_id = self.cursor.fetchall() + if len(path_id) != 1: + log.error('Found wrong number of path ids: %s for path %s, abort', + path_id, path) + return + query = ''' + SELECT idSong + FROM song + WHERE strFileName = ? AND idPath = ? + ''' + self.cursor.execute(query, (filename, path_id[0])) + song_id = self.cursor.fetchall() + if len(song_id) != 1: + log.info('Found wrong number of songs %s, abort', song_id) + return + return song_id[0] + def getUnplayedItems(self): """ VIDEOS @@ -1522,24 +1550,29 @@ class Kodidb_Functions(): self.cursor.execute(query, (kodi_id, kodi_type)) -def get_kodiid_from_filename(file): +def kodiid_from_filename(path, kodi_type): """ - Returns the tuple (kodiid, type) if we have a video in the database with - said filename, or (None, None) + Returns kodi_id if we have an item in the Kodi video or audio database with + said path. Feed with the Kodi itemtype, e.v. 'movie', 'song' + Returns None if not possible """ - kodiid = None - typus = None + kodi_id = None try: - filename = file.rsplit('/', 1)[1] - path = file.rsplit('/', 1)[0] + '/' + filename = path.rsplit('/', 1)[1] + path = path.rsplit('/', 1)[0] + '/' except IndexError: - filename = file.rsplit('\\', 1)[1] - path = file.rsplit('\\', 1)[0] + '\\' - log.debug('Trying to figure out playing item from filename: %s ' - 'and path: %s' % (filename, path)) - with GetKodiDB('video') as kodi_db: - try: - kodiid, typus = kodi_db.getIdFromFilename(filename, path) - except TypeError: - log.info('No kodi video element found with filename %s' % filename) - return (kodiid, typus) + filename = path.rsplit('\\', 1)[1] + path = path.rsplit('\\', 1)[0] + '\\' + if kodi_type == v.KODI_TYPE_SONG: + with GetKodiDB('music') as kodi_db: + try: + kodi_id, _ = kodi_db.music_id_from_filename(filename, path) + except TypeError: + log.info('No Kodi audio db element found for path %s', path) + else: + with GetKodiDB('video') as kodi_db: + try: + kodi_id, _ = kodi_db.video_id_from_filename(filename, path) + except TypeError: + log.info('No kodi video db element found for path %s', path) + return kodi_id diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 3cb82380..d5d2fd00 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -11,7 +11,7 @@ import plexdb_functions as plexdb from utils import window, settings, CatchExceptions, tryDecode, tryEncode, \ plex_command from PlexFunctions import scrobble -from kodidb_functions import get_kodiid_from_filename +from kodidb_functions import kodiid_from_filename from PlexAPI import API import json_rpc as js import state @@ -185,68 +185,61 @@ class KodiMonitor(Monitor): u'item': {u'type': u'movie', u'title': u''}, u'player': {u'playerid': 1, u'speed': 1} } + Unfortunately VERY random inputs! + E.g. when using Widgets, Kodi doesn't tell us shit """ - log.debug('PlayBackStart called with: %s', data) # Get the type of media we're playing try: kodi_type = data['item']['type'] playerid = data['player']['playerid'] - json_data = js.get_item(playerid) except (TypeError, KeyError): - log.info('Aborting playback report - item is invalid for updates') + log.info('Aborting playback report - item invalid for updates %s', + data) return + json_data = js.get_item(playerid) + path = json_data.get('file') + kodi_id = json_data.get('id') + if not path and not kodi_id: + log.info('Aborting playback report - no Kodi id or file for %s', + json_data) + return + # Plex id will NOT be set with direct paths + plex_id = state.PLEX_IDS.get(path) try: - kodi_id = json_data['id'] - kodi_type = json_data['type'] + plex_type = v.PLEX_TYPE_FROM_KODI_TYPE[kodi_type] except KeyError: - log.info('Aborting playback report - no Kodi id for %s', json_data) - return - # Get Plex' item id - with plexdb.Get_Plex_DB() as plex_db: - plex_dbitem = plex_db.getItem_byKodiId(kodi_id, kodi_type) - try: - plex_id = plex_dbitem[0] - plex_type = plex_dbitem[2] - except TypeError: - # No plex id, hence item not in the library. E.g. clips - plex_id = None plex_type = None + # No Kodi id returned by Kodi, even if there is one. Ex: Widgets + if plex_id and not kodi_id: + with plexdb.Get_Plex_DB() as plex_db: + plex_dbitem = plex_db.getItem_byId(plex_id) + try: + kodi_id = plex_dbitem[0] + except TypeError: + kodi_id = None + # If using direct paths and starting playback from a widget + if not path.startswith('http'): + if not kodi_id: + kodi_id = kodiid_from_filename(path, kodi_type) + if not plex_id and kodi_id: + with plexdb.Get_Plex_DB() as plex_db: + plex_dbitem = plex_db.getItem_byKodiId(kodi_id, kodi_type) + try: + plex_id = plex_dbitem[0] + plex_type = plex_dbitem[2] + except TypeError: + # No plex id, hence item not in the library. E.g. clips + pass state.PLAYER_STATES[playerid].update(js.get_player_props(playerid)) state.PLAYER_STATES[playerid]['file'] = json_data['file'] state.PLAYER_STATES[playerid]['kodi_id'] = kodi_id state.PLAYER_STATES[playerid]['kodi_type'] = kodi_type state.PLAYER_STATES[playerid]['plex_id'] = plex_id state.PLAYER_STATES[playerid]['plex_type'] = plex_type - log.debug('Set the player state %s', state.PLAYER_STATES[playerid]) # Set other stuff like volume state.PLAYER_STATES[playerid]['volume'] = js.get_volume() state.PLAYER_STATES[playerid]['muted'] = js.get_muted() - return - - # Switch subtitle tracks if applicable - subtitle = window('plex_%s.subtitle' % tryEncode(currentFile)) - if window(tryEncode('plex_%s.playmethod' % currentFile)) \ - == 'Transcode' and subtitle: - if window('plex_%s.subtitle' % currentFile) == 'None': - self.xbmcplayer.showSubtitles(False) - else: - self.xbmcplayer.setSubtitleStream(int(subtitle)) - - # Set some stuff if Kodi initiated playback - if ((settings('useDirectPaths') == "1" and not typus == "song") - or - (typus == "song" and settings('enableMusic') == "true")): - if self.StartDirectPath(plex_id, - typus, - tryEncode(currentFile)) is False: - log.error('Could not initiate monitoring; aborting') - return - - # Save currentFile for cleanup later and to be able to access refs - window('plex_lastPlayedFiled', value=currentFile) - window('plex_currently_playing_itemid', value=plex_id) - window("plex_%s.itemid" % tryEncode(currentFile), value=plex_id) - log.info('Finish playback startup') + log.debug('Set the player state: %s', state.PLAYER_STATES[playerid]) def StartDirectPath(self, plex_id, type, currentFile): """ diff --git a/resources/lib/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py index 4183428d..d45744d3 100644 --- a/resources/lib/plexbmchelper/subscribers.py +++ b/resources/lib/plexbmchelper/subscribers.py @@ -1,56 +1,68 @@ -import logging -import re -import threading - -from xbmc import sleep +""" +Manages getting playstate from Kodi and sending it to the PMS as well as +subscribed Plex Companion clients. +""" +from logging import getLogger +from re import sub +from threading import Thread, RLock import downloadutils -from clientinfo import getXArgsDeviceInfo from utils import window, kodi_time_to_millis -import PlexFunctions as pf import state import variables as v import json_rpc as js ############################################################################### -log = logging.getLogger("PLEX."+__name__) +LOG = getLogger("PLEX." + __name__) ############################################################################### # What is Companion controllable? CONTROLLABLE = { v.PLEX_TYPE_PHOTO: 'skipPrevious,skipNext,stop', - v.PLEX_TYPE_AUDIO: 'playPause,stop,volume,shuffle,repeat,seekTo,' \ + v.PLEX_TYPE_AUDIO: 'playPause,stop,volume,shuffle,repeat,seekTo,' 'skipPrevious,skipNext,stepBack,stepForward', - v.PLEX_TYPE_VIDEO: 'playPause,stop,volume,audioStream,subtitleStream,' \ - 'seekTo,skipPrevious,skipNext,stepBack,stepForward' + v.PLEX_TYPE_VIDEO: 'playPause,stop,volume,shuffle,audioStream,' + 'subtitleStream,seekTo,skipPrevious,skipNext,stepBack,stepForward' } class SubscriptionManager: + """ + Manages Plex companion subscriptions + """ def __init__(self, RequestMgr, player, mgr): self.serverlist = [] self.subscribers = {} self.info = {} - self.lastkey = "" - self.containerKey = "" - self.ratingkey = "" - self.lastplayers = {} - self.lastinfo = { - 'video': {}, - 'audio': {}, - 'picture': {} - } + self.containerKey = None + self.ratingkey = None self.server = "" self.protocol = "http" self.port = "" - self.playerprops = {} - self.doUtils = downloadutils.DownloadUtils().downloadUrl + # In order to be able to signal a stop at the end + self.last_params = {} + self.lastplayers = {} + + self.doUtils = downloadutils.DownloadUtils self.xbmcplayer = player self.playqueue = mgr.playqueue - self.RequestMgr = RequestMgr + @staticmethod + def _headers(): + """ + Headers are different for Plex Companion! + """ + return { + 'Content-type': 'text/plain', + 'Connection': 'Keep-Alive', + 'Keep-Alive': 'timeout=20', + 'X-Plex-Client-Identifier': v.PKC_MACHINE_IDENTIFIER, + 'Access-Control-Expose-Headers': 'X-Plex-Client-Identifier', + 'X-Plex-Protocol': "1.0" + } + def getServerByHost(self, host): if len(self.serverlist) == 1: return self.serverlist[0] @@ -61,64 +73,80 @@ class SubscriptionManager: return {} def msg(self, players): - log.debug('players: %s', players) + LOG.debug('players: %s', players) msg = v.XML_HEADER msg += '\n' % (CONTROLLABLE[ptype], ptype, ptype) + playerid = player['playerid'] + info = state.PLAYER_STATES[playerid] + status = 'paused' if info['speed'] == '0' else 'playing' + ret = ' 30: - sub.cleanup() - del self.subscribers[sub.uuid] - - def getPlayerProperties(self, playerid): - # Get the playqueue - playqueue = self.playqueue.playqueues[playerid] - # get info from the player - props = state.PLAYER_STATES[playerid] - info = { - 'time': kodi_time_to_millis(props['time']), - 'duration': kodi_time_to_millis(props['totaltime']), - 'state': ("paused", "playing")[int(props['speed'])], - 'shuffle': ("0", "1")[props.get('shuffled', False)], - 'repeat': v.PLEX_REPEAT_FROM_KODI_REPEAT[props.get('repeat')] - } - pos = props['position'] - try: - info['playQueueItemID'] = playqueue.items[pos].ID or 'null' - info['guid'] = playqueue.items[pos].guid or 'null' - info['playQueueID'] = playqueue.ID or 'null' - info['playQueueVersion'] = playqueue.version or 'null' - info['itemType'] = playqueue.items[pos].plex_type or 'null' - except: - info['itemType'] = props.get('type') or 'null' - - # get the volume from the application - info['volume'] = js.get_volume() - info['mute'] = js.get_muted() - - info['plex_transient_token'] = playqueue.plex_transient_token - - return info + with RLock(): + for subscriber in self.subscribers.values(): + if subscriber.age > 30: + subscriber.cleanup() + del self.subscribers[subscriber.uuid] class Subscriber: @@ -268,16 +267,13 @@ class Subscriber: self.commandID = int(commandID) or 0 self.navlocationsent = False self.age = 0 - self.doUtils = downloadutils.DownloadUtils().downloadUrl + self.doUtils = downloadutils.DownloadUtils self.subMgr = subMgr self.RequestMgr = RequestMgr def __eq__(self, other): return self.uuid == other.uuid - def tostr(self): - return "uuid=%s,commandID=%i" % (self.uuid, self.commandID) - def cleanup(self): self.RequestMgr.closeConnection(self.protocol, self.host, self.port) @@ -289,11 +285,12 @@ class Subscriber: return True else: self.navlocationsent = True - msg = re.sub(r"INSERTCOMMANDID", str(self.commandID), msg) - log.debug("sending xml to subscriber %s:\n%s" % (self.tostr(), msg)) + msg = sub(r"INSERTCOMMANDID", str(self.commandID), msg) + LOG.debug("sending xml to subscriber uuid=%s,commandID=%i:\n%s", + self.uuid, self.commandID, msg) url = self.protocol + '://' + self.host + ':' + self.port \ + "/:/timeline" - t = threading.Thread(target=self.threadedSend, args=(url, msg)) + t = Thread(target=self.threadedSend, args=(url, msg)) t.start() def threadedSend(self, url, msg): @@ -301,9 +298,8 @@ class Subscriber: Threaded POST request, because they stall due to PMS response missing the Content-Length header :-( """ - response = self.doUtils(url, - postBody=msg, - action_type="POST") - log.debug('response is: %s', response) + response = self.doUtils().downloadUrl(url, + postBody=msg, + action_type="POST") if response in [False, None, 401]: self.subMgr.removeSubscriber(self.uuid) diff --git a/resources/lib/state.py b/resources/lib/state.py index 5d08c32f..a115f5bc 100644 --- a/resources/lib/state.py +++ b/resources/lib/state.py @@ -112,6 +112,9 @@ PLAYER_STATES = { 2: {}, 3: {} } +# Dict containing all filenames as keys with plex id as values - used for addon +# paths for playback (since we're not receiving a Kodi id) +PLEX_IDS = {} PLAYED_INFO = {} # Kodi webserver details diff --git a/resources/lib/variables.py b/resources/lib/variables.py index d660f22f..3dace6f4 100644 --- a/resources/lib/variables.py +++ b/resources/lib/variables.py @@ -199,6 +199,20 @@ KODITYPE_FROM_PLEXTYPE = { 'XXXXXXX': 'genre' } +PLEX_TYPE_FROM_KODI_TYPE = { + KODI_TYPE_VIDEO: PLEX_TYPE_VIDEO, + KODI_TYPE_MOVIE: PLEX_TYPE_MOVIE, + KODI_TYPE_EPISODE: PLEX_TYPE_EPISODE, + KODI_TYPE_SEASON: PLEX_TYPE_SEASON, + KODI_TYPE_SHOW: PLEX_TYPE_SHOW, + KODI_TYPE_CLIP: PLEX_TYPE_CLIP, + KODI_TYPE_ARTIST: PLEX_TYPE_ARTIST, + KODI_TYPE_ALBUM: PLEX_TYPE_ALBUM, + KODI_TYPE_SONG: PLEX_TYPE_SONG, + KODI_TYPE_AUDIO: PLEX_TYPE_AUDIO, + KODI_TYPE_PHOTO: PLEX_TYPE_PHOTO +} + KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE = { PLEX_TYPE_VIDEO: KODI_TYPE_VIDEO, PLEX_TYPE_MOVIE: KODI_TYPE_VIDEO,