Playqueues major haul-over

This commit is contained in:
tomkat83 2016-12-27 17:33:52 +01:00
parent 95c87065ed
commit 0c2d4984ab
14 changed files with 673 additions and 678 deletions

View file

@ -11,7 +11,6 @@ from plexbmchelper import listener, plexgdm, subscribers, functions, \
httppersist, plexsettings
from PlexFunctions import ParseContainerKey, GetPlayQueue, \
ConvertPlexToKodiTime
import playlist
import player
###############################################################################
@ -25,18 +24,18 @@ log = logging.getLogger("PLEX."+__name__)
@ThreadMethods
class PlexCompanion(Thread):
"""
Initialize with a Queue for callbacks
"""
def __init__(self):
def __init__(self, callback=None):
log.info("----===## Starting PlexCompanion ##===----")
if callback is not None:
self.mgr = callback
self.playqueue = self.mgr.playqueue
self.settings = plexsettings.getSettings()
# Start GDM for server/client discovery
self.client = plexgdm.plexgdm()
self.client.clientDetails(self.settings)
log.debug("Registration string is: %s "
% self.client.getClientDetails())
# Initialize playlist/queue stuff
self.playlist = playlist.Playlist('video')
# kodi player instance
self.player = player.Player()
@ -72,49 +71,44 @@ class PlexCompanion(Thread):
data = task['data']
if task['action'] == 'playlist':
# Get the playqueue ID
try:
_, queueId, query = ParseContainerKey(data['containerKey'])
_, ID, query = ParseContainerKey(data['containerKey'])
except Exception as e:
log.error('Exception while processing: %s' % e)
import traceback
log.error("Traceback:\n%s" % traceback.format_exc())
return
if self.playlist is not None:
if self.playlist.Typus() != data.get('type'):
log.debug('Switching to Kodi playlist of type %s'
% data.get('type'))
self.playlist = None
if self.playlist is None:
if data.get('type') == 'music':
self.playlist = playlist.Playlist('music')
else:
self.playlist = playlist.Playlist('video')
if queueId != self.playlist.QueueId():
self.mgr.playqueue.update_playqueue_with_companion(data)
self.playqueue = self.mgr.playqueue.get_playqueue_from_plextype(
data.get('type'))
if queueId != self.playqueue.ID:
log.info('New playlist received, updating!')
xml = GetPlayQueue(queueId)
if xml in (None, 401):
log.error('Could not download Plex playlist.')
return
# Clear existing playlist on the Kodi side
self.playlist.clear()
self.playqueue.clear()
# Set new values
self.playlist.QueueId(queueId)
self.playlist.PlayQueueVersion(int(
self.playqueue.QueueId(queueId)
self.playqueue.PlayQueueVersion(int(
xml.attrib.get('playQueueVersion')))
self.playlist.Guid(xml.attrib.get('guid'))
self.playqueue.Guid(xml.attrib.get('guid'))
items = []
for item in xml:
items.append({
'playQueueItemID': item.get('playQueueItemID'),
'plexId': item.get('ratingKey'),
'kodiId': None})
self.playlist.playAll(
self.playqueue.playAll(
items,
startitem=self._getStartItem(data.get('key', '')),
offset=ConvertPlexToKodiTime(data.get('offset', 0)))
log.info('Initiated playlist no %s with version %s'
% (self.playlist.QueueId(),
self.playlist.PlayQueueVersion()))
% (self.playqueue.QueueId(),
self.playqueue.PlayQueueVersion()))
else:
log.error('This has never happened before!')
@ -129,7 +123,7 @@ class PlexCompanion(Thread):
requestMgr = httppersist.RequestMgr()
jsonClass = functions.jsonClass(requestMgr, self.settings)
subscriptionManager = subscribers.SubscriptionManager(
jsonClass, requestMgr, self.player, self.playlist)
jsonClass, requestMgr, self.player, self.playqueue)
queue = Queue.Queue(maxsize=100)

View file

@ -60,11 +60,12 @@ KODITYPE_FROM_PLEXTYPE = {
'XXXXXXX': 'genre'
}
KODIAUDIOVIDEO_FROM_MEDIA_TYPE = {
KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE = {
'movie': 'video',
'episode': 'video',
'season': 'video',
'tvshow': 'video',
'clip': 'video',
'artist': 'audio',
'album': 'audio',
'track': 'audio',

View file

@ -19,7 +19,6 @@ import clientinfo
import downloadutils
import embydb_functions as embydb
import playbackutils as pbutils
import playlist
import PlexFunctions
import PlexAPI

View file

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

View file

@ -14,7 +14,6 @@ import kodidb_functions as kodidb
import playbackutils as pbutils
from utils import window, settings, CatchExceptions, tryDecode, tryEncode
from PlexFunctions import scrobble, REMAP_TYPE_FROM_PLEXTYPE
from playlist import Playlist
###############################################################################
@ -25,11 +24,11 @@ log = logging.getLogger("PLEX."+__name__)
class KodiMonitor(xbmc.Monitor):
def __init__(self):
def __init__(self, callback):
self.mgr = callback
self.doUtils = downloadutils.DownloadUtils().downloadUrl
self.xbmcplayer = xbmc.Player()
self.playlist = Playlist('video')
self.playqueue = self.mgr.playqueue
xbmc.Monitor.__init__(self)
log.info("Kodi monitor started.")
@ -173,7 +172,6 @@ class KodiMonitor(xbmc.Monitor):
# Data : {u'item': {u'type': u'movie', u'id': 3}, u'playlistid': 1,
# u'position': 0}
self.playlist.kodi_onadd(data)
Playlist()
def PlayBackStart(self, data):
"""

View file

@ -356,19 +356,10 @@ class ProcessFanartThread(Thread):
@ThreadMethods
class LibrarySync(Thread):
"""
librarysync.LibrarySync(queue)
where (communication with websockets)
queue: Queue object for background sync
"""
# Borg, even though it's planned to only have 1 instance up and running!
_shared_state = {}
def __init__(self, callback=None):
self.mgr = callback
def __init__(self, queue):
self.__dict__ = self._shared_state
# Communication with websockets
self.queue = queue
self.itemsToProcess = []
self.sessionKeys = []
self.fanartqueue = Queue.Queue()
@ -1720,7 +1711,8 @@ class LibrarySync(Thread):
xbmcplayer = xbmc.Player()
queue = self.queue
# Link to Websocket queue
queue = self.mgr.ws.queue
startupComplete = False
self.views = []

View file

@ -11,7 +11,7 @@ import xbmcgui
import xbmcplugin
import playutils as putils
import playlist
from playqueue import Playqueue
from utils import window, settings, tryEncode, tryDecode
import downloadutils
@ -37,10 +37,7 @@ class PlaybackUtils():
self.userid = window('currUserId')
self.server = window('pms_server')
if self.API.getType() == 'track':
self.pl = playlist.Playlist(typus='music')
else:
self.pl = playlist.Playlist(typus='video')
self.pl = Playqueue().get_playqueue_from_plextype(self.API.getType())
def play(self, itemid, dbid=None):
@ -89,7 +86,7 @@ class PlaybackUtils():
contextmenu_play = window('plex_contextplay') == 'true'
window('plex_contextplay', clear=True)
homeScreen = xbmc.getCondVisibility('Window.IsActive(home)')
kodiPl = self.pl.playlist
kodiPl = self.pl.kodi_pl
sizePlaylist = kodiPl.size()
if contextmenu_play:
# Need to start with the items we're inserting here

View file

@ -1,565 +0,0 @@
# -*- coding: utf-8 -*-
###############################################################################
import logging
import json
from urllib import urlencode
from threading import Lock
from functools import wraps
from urllib import quote, urlencode
import xbmc
import embydb_functions as embydb
import kodidb_functions as kodidb
from utils import window, tryEncode, JSONRPC
import playbackutils
import PlexFunctions as PF
import PlexAPI
from downloadutils import DownloadUtils
###############################################################################
log = logging.getLogger("PLEX."+__name__)
###############################################################################
PLEX_PLAYQUEUE_ARGS = (
'playQueueID',
'playQueueVersion',
'playQueueSelectedItemID',
'playQueueSelectedItemOffset'
)
class lockMethod:
"""
Decorator for class methods to lock hem completely. Same lock is used for
every single decorator and instance used!
Here only used for Playlist()
"""
lock = Lock()
@classmethod
def decorate(cls, func):
@wraps(func)
def wrapper(*args, **kwargs):
with cls.lock:
result = func(*args, **kwargs)
return result
return wrapper
class Playlist():
"""
Initiate with Playlist(typus='video' or 'music')
ATTRIBUTES:
id: integer
position: integer, default -1
type: string, default "unknown"
"unknown",
"video",
"audio",
"picture",
"mixed"
size: integer
"""
# Borg - multiple instances, shared state
_shared_state = {}
player = xbmc.Player()
playlists = None
@lockMethod.decorate
def __init__(self, typus=None):
# Borg
self.__dict__ = self._shared_state
# If already initiated, return
if self.playlists is not None:
return
self.doUtils = DownloadUtils().downloadUrl
# Get all playlists from Kodi
self.playlists = JSONRPC('Playlist.GetPlaylists').execute()
try:
self.playlists = self.playlists['result']
except KeyError:
log.error('Could not get Kodi playlists. JSON Result was: %s'
% self.playlists)
self.playlists = None
return
# Example return: [{u'playlistid': 0, u'type': u'audio'},
# {u'playlistid': 1, u'type': u'video'},
# {u'playlistid': 2, u'type': u'picture'}]
# Initiate the Kodi playlists
for playlist in self.playlists:
# Initialize each Kodi playlist
if playlist['type'] == 'audio':
playlist['kodi_pl'] = xbmc.PlayList(xbmc.PLAYLIST_MUSIC)
elif playlist['type'] == 'video':
playlist['kodi_pl'] = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
else:
# Currently, only video or audio playlists available
playlist['kodi_pl'] = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
# Initialize Plex info on the playQueue
for arg in PLEX_PLAYQUEUE_ARGS:
playlist[arg] = None
# Build a list of all items within each playlist
playlist['items'] = []
for item in self._get_kodi_items(playlist['playlistid']):
playlist['items'].append({
'kodi_id': item.get('id'),
'type': item['type'],
'file': item['file'],
'playQueueItemID': None,
'plex_id': self._get_plexid(item)
})
log.debug('self.playlist: %s' % playlist)
def _init_pl_item(self):
return {
'plex_id': None,
'kodi_id': None,
'file': None,
'type': None, # 'audio' or 'video'
'playQueueItemID': None,
'uri': None,
# To be able to drag Kodi JSON data along:
'playlistid': None,
'position': None,
'item': None,
}
def _get_plexid(self, item):
"""
Supply with data['item'] as returned from Kodi JSON-RPC interface
"""
with embydb.GetEmbyDB() as emby_db:
emby_dbitem = emby_db.getItem_byKodiId(item.get('id'),
item.get('type'))
try:
plex_id = emby_dbitem[0]
except TypeError:
plex_id = None
return plex_id
def _get_kodi_items(self, playlistid):
params = {
'playlistid': playlistid,
'properties': ["title", "file"]
}
answ = JSONRPC('Playlist.GetItems').execute(params)
# returns e.g. [{u'title': u'3 Idiots', u'type': u'movie', u'id': 3,
# u'file': u'smb://nas/PlexMovies/3 Idiots 2009 pt1.mkv', u'label':
# u'3 Idiots'}]
try:
answ = answ['result']['items']
except KeyError:
answ = []
return answ
@lockMethod.decorate
def getQueueIdFromPosition(self, playlistPosition):
return self.items[playlistPosition]['playQueueItemID']
@lockMethod.decorate
def Typus(self, value=None):
if value:
self.typus = value
else:
return self.typus
@lockMethod.decorate
def PlayQueueVersion(self, value=None):
if value:
self.playQueueVersion = value
else:
return self.playQueueVersion
@lockMethod.decorate
def QueueId(self, value=None):
if value:
self.queueId = value
else:
return self.queueId
@lockMethod.decorate
def Guid(self, value=None):
if value:
self.guid = value
else:
return self.guid
@lockMethod.decorate
def clear(self):
"""
Empties current Kodi playlist and associated variables
"""
log.info('Clearing playlist')
self.playlist.clear()
self.items = []
self.queueId = None
self.playQueueVersion = None
self.guid = None
def _initiatePlaylist(self):
log.info('Initiating playlist')
playlist = None
with embydb.GetEmbyDB() as emby_db:
for item in self.items:
itemid = item['plexId']
embydb_item = emby_db.getItem_byId(itemid)
try:
mediatype = embydb_item[4]
except TypeError:
log.info('Couldnt find item %s in Kodi db' % itemid)
item = PF.GetPlexMetadata(itemid)
if item in (None, 401):
log.info('Couldnt find item %s on PMS, trying next'
% itemid)
continue
if PlexAPI.API(item[0]).getType() == 'track':
playlist = xbmc.PlayList(xbmc.PLAYLIST_MUSIC)
log.info('Music playlist initiated')
self.typus = 'music'
else:
playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
log.info('Video playlist initiated')
self.typus = 'video'
else:
if mediatype == 'song':
playlist = xbmc.PlayList(xbmc.PLAYLIST_MUSIC)
log.info('Music playlist initiated')
self.typus = 'music'
else:
playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
log.info('Video playlist initiated')
self.typus = 'video'
break
self.playlist = playlist
if self.playlist is not None:
self.playlistId = self.playlist.getPlayListId()
def _processItems(self, startitem, startPlayer=False):
startpos = None
with embydb.GetEmbyDB() as emby_db:
for pos, item in enumerate(self.items):
kodiId = None
plexId = item['plexId']
embydb_item = emby_db.getItem_byId(plexId)
try:
kodiId = embydb_item[0]
mediatype = embydb_item[4]
except TypeError:
log.info('Couldnt find item %s in Kodi db' % plexId)
xml = PF.GetPlexMetadata(plexId)
if xml in (None, 401):
log.error('Could not download plexId %s' % plexId)
else:
log.debug('Downloaded xml metadata, adding now')
self._addtoPlaylist_xbmc(xml[0])
else:
# Add to playlist
log.debug("Adding %s PlexId %s, KodiId %s to playlist."
% (mediatype, plexId, kodiId))
self._addtoPlaylist(kodiId, mediatype)
# Add the kodiId
if kodiId is not None:
item['kodiId'] = str(kodiId)
if (startpos is None and startitem[1] == item[startitem[0]]):
startpos = pos
if startPlayer is True and len(self.playlist) > 0:
if startpos is not None:
self.player.play(self.playlist, startpos=startpos)
else:
log.info('Never received a starting item for playlist, '
'starting with the first entry')
self.player.play(self.playlist)
@lockMethod.decorate
def playAll(self, items, startitem, offset):
"""
items: list of dicts of the form
{
'playQueueItemID': Plex playQueueItemID, e.g. '29175'
'plexId': Plex ratingKey, e.g. '125'
'kodiId': Kodi's db id of the same item
}
startitem: tuple (typus, id), where typus is either
'playQueueItemID' or 'plexId' and id is the corresponding
id as a string
offset: First item's time offset to play in Kodi time (an int)
"""
log.info("---*** PLAY ALL ***---")
log.debug('Startitem: %s, offset: %s, items: %s'
% (startitem, offset, items))
self.items = items
if self.playlist is None:
self._initiatePlaylist()
if self.playlist is None:
log.error('Could not create playlist, abort')
return
window('plex_customplaylist', value="true")
if offset != 0:
# Seek to the starting position
window('plex_customplaylist.seektime', str(offset))
self._processItems(startitem, startPlayer=True)
# Log playlist
self._verifyPlaylist()
log.debug('Internal playlist: %s' % self.items)
@lockMethod.decorate
def modifyPlaylist(self, itemids):
log.info("---*** MODIFY PLAYLIST ***---")
log.debug("Items: %s" % itemids)
self._initiatePlaylist(itemids)
self._processItems(itemids, startPlayer=True)
self._verifyPlaylist()
@lockMethod.decorate
def addtoPlaylist(self, dbid=None, mediatype=None, url=None):
"""
mediatype: Kodi type: 'movie', 'episode', 'musicvideo', 'artist',
'album', 'song', 'genre'
"""
self._addtoPlaylist(dbid, mediatype, url)
def _addtoPlaylist(self, dbid=None, mediatype=None, url=None):
pl = {
'jsonrpc': "2.0",
'id': 1,
'method': "Playlist.Add",
'params': {
'playlistid': self.playlistId
}
}
if dbid is not None:
pl['params']['item'] = {'%sid' % tryEncode(mediatype): int(dbid)}
else:
pl['params']['item'] = {'file': url}
log.debug(xbmc.executeJSONRPC(json.dumps(pl)))
def _addtoPlaylist_xbmc(self, item):
API = PlexAPI.API(item)
params = {
'mode': "play",
'dbid': 'plextrailer',
'id': API.getRatingKey(),
'filename': API.getKey()
}
playurl = "plugin://plugin.video.plexkodiconnect.movies/?%s" \
% urlencode(params)
listitem = API.CreateListItemFromPlexItem()
playbackutils.PlaybackUtils(item).setArtwork(listitem)
self.playlist.add(playurl, listitem)
@lockMethod.decorate
def insertintoPlaylist(self,
position,
dbid=None,
mediatype=None,
url=None):
pl = {
'jsonrpc': "2.0",
'id': 1,
'method': "Playlist.Insert",
'params': {
'playlistid': self.playlistId,
'position': position
}
}
if dbid is not None:
pl['params']['item'] = {'%sid' % tryEncode(mediatype): int(dbid)}
else:
pl['params']['item'] = {'file': url}
log.debug(xbmc.executeJSONRPC(json.dumps(pl)))
@lockMethod.decorate
def verifyPlaylist(self):
self._verifyPlaylist()
def _verifyPlaylist(self):
pl = {
'jsonrpc': "2.0",
'id': 1,
'method': "Playlist.GetItems",
'params': {
'playlistid': self.playlistId,
'properties': ['title', 'file']
}
}
log.debug(xbmc.executeJSONRPC(json.dumps(pl)))
@lockMethod.decorate
def removefromPlaylist(self, position):
pl = {
'jsonrpc': "2.0",
'id': 1,
'method': "Playlist.Remove",
'params': {
'playlistid': self.playlistId,
'position': position
}
}
log.debug(xbmc.executeJSONRPC(json.dumps(pl)))
def _get_uri(self, plex_id=None, item=None):
"""
Supply with either plex_id or data['item'] as received from Kodi JSON-
RPC
"""
uri = None
if plex_id is None:
plex_id = self._get_plexid(item)
self._cur_item['plex_id'] = plex_id
if plex_id is not None:
xml = PF.GetPlexMetadata(plex_id)
try:
uri = ('library://%s/item/%s%s' %
(xml.attrib.get('librarySectionUUID'),
quote('library/metadata/', safe=''), plex_id))
except:
pass
if uri is None:
try:
uri = 'library://whatever/item/%s' % quote(item['file'],
safe='')
except:
raise KeyError('Could not get file/url with item: %s' % item)
self._cur_item['uri'] = uri
return uri
def _init_plex_playQueue(self, plex_id=None, data=None):
"""
Supply either plex_id or the data supplied by Kodi JSON-RPC
"""
if plex_id is None:
plex_id = self._get_plexid(data['item'])
self._cur_item['plex_id'] = plex_id
if data is not None:
playlistid = data['playlistid']
plex_type = self.playlists[playlistid]['type']
else:
with embydb.GetEmbyDB() as emby_db:
plex_type = emby_db.getItem_byId(plex_id)
try:
plex_type = PF.KODIAUDIOVIDEO_FROM_MEDIA_TYPE[plex_type[4]]
except TypeError:
raise KeyError('Unknown plex_type %s' % plex_type)
for playlist in self.playlists:
if playlist['type'] == plex_type:
playlistid = playlist['playlistid']
self._cur_item['playlistid'] = playlistid
self._cur_item['type'] = plex_type
params = {
'next': 0,
'type': plex_type,
'uri': self._get_uri(plex_id=plex_id, item=data['item'])
}
log.debug('params: %s' % urlencode(params))
xml = self.doUtils(url="{server}/playQueues",
action_type="POST",
parameters=params)
try:
xml.attrib
except (TypeError, AttributeError):
raise KeyError('Could not post to PMS, received: %s' % xml)
self._Plex_item_updated(xml)
def _Plex_item_updated(self, xml):
"""
Called if a new item has just been added/updated @ Plex playQueue
Call with the PMS' xml reply
"""
# Update the ITEM
log.debug('xml.attrib: %s' % xml.attrib)
args = {
'playQueueItemID': 'playQueueLastAddedItemID', # for playlist PUT
'playQueueItemID': 'playQueueSelectedItemID' # for playlist INIT
}
for old, new in args.items():
if new in xml.attrib:
self._cur_item[old] = xml.attrib[new]
# Update the PLAYLIST
for arg in PLEX_PLAYQUEUE_ARGS:
if arg in xml.attrib:
self.playlists[self._cur_item['playlistid']][arg] = xml.attrib[arg]
def _init_Kodi_item(self, item):
"""
Call with Kodi's JSON-RPC data['item']
"""
self._cur_item['kodi_id'] = item.get('id')
try:
self._cur_item['type'] = PF.KODIAUDIOVIDEO_FROM_MEDIA_TYPE[
item.get('type')]
except KeyError:
log.error('Could not get media_type for %s' % item)
def _add_curr_item(self):
self.playlists[self._cur_item['playlistid']]['items'].insert(
self._cur_item['position'],
self._cur_item)
@lockMethod.decorate
def kodi_onadd(self, data):
"""
Called if Kodi playlist is modified. Data is Kodi JSON-RPC output, e.g.
{
u'item': {u'type': u'movie', u'id': 3},
u'playlistid': 1,
u'position': 0
}
"""
self._cur_item = self._init_pl_item()
self._cur_item.update(data)
self._init_Kodi_item(data['item'])
pl = self.playlists[data['playlistid']]
if pl['playQueueID'] is None:
# Playlist needs to be initialized!
try:
self._init_plex_playQueue(data=data)
except KeyError as e:
log.error('Error encountered while init playQueue: %s' % e)
return
else:
next_item = data['position']
if next_item != 0:
next_item = pl['items'][data['position']-1]['playQueueItemID']
params = {
'next': next_item,
'type': pl['type'],
'uri': self._get_uri(item=data['item'])
}
xml = self.doUtils(url="{server}/playQueues/%s"
% pl['playQueueID'],
action_type="PUT",
parameters=params)
try:
xml.attrib
except AttributeError:
log.error('Could not add item %s to playQueue' % data)
return
self._Plex_item_updated(xml)
# Add the new item to our playlist
self._add_curr_item()
log.debug('self.playlists are now: %s' % self.playlists)

View file

@ -0,0 +1,394 @@
import logging
from urllib import quote
import embydb_functions as embydb
from downloadutils import DownloadUtils as DU
from utils import JSONRPC, tryEncode
###############################################################################
log = logging.getLogger("PLEX."+__name__)
###############################################################################
class Playlist_Object_Baseclase(object):
playlistid = None # Kodi playlist ID, [int]
type = None # Kodi type: 'audio', 'video', 'picture'
kodi_pl = None # Kodi xbmc.PlayList object
items = [] # list of PLAYLIST_ITEMS
old_kodi_pl = [] # to store old Kodi JSON result with all pl items
ID = None # Plex id, e.g. playQueueID
version = None # Plex version, [int]
selectedItemID = None
selectedItemOffset = None
shuffled = 0 # [int], 0: not shuffled, 1: ??? 2: ???
repeat = 0 # [int], 0: not repeated, 1: ??? 2: ???
def __repr__(self):
answ = "<%s object: " % (self.__class__.__name__)
for key in self.__dict__:
answ += '%s: %s, ' % (key, getattr(self, key))
return answ[:-2] + ">"
class Playlist_Object(Playlist_Object_Baseclase):
kind = 'playList'
class Playqueue_Object(Playlist_Object_Baseclase):
kind = 'playQueue'
class Playlist_Item(object):
ID = None # Plex playlist/playqueue id, e.g. playQueueItemID
plex_id = None # Plex unique item id, "ratingKey"
plex_UUID = None # Plex librarySectionUUID
kodi_id = None # Kodi unique kodi id (unique only within type!)
kodi_type = None # Kodi type: 'movie'
file = None # Path to the item's file
uri = None # Weird Plex uri path involving plex_UUID
def playlist_item_from_kodi_item(kodi_item):
"""
Turns the JSON answer from Kodi into a playlist element
Supply with data['item'] as returned from Kodi JSON-RPC interface.
kodi_item dict contains keys 'id', 'type', 'file' (if applicable)
"""
item = Playlist_Item()
if kodi_item.get('id'):
item.kodi_id = kodi_item['id']
with embydb.GetEmbyDB() as emby_db:
emby_dbitem = emby_db.getItem_byKodiId(kodi_item['id'],
kodi_item['type'])
try:
item.plex_id = emby_dbitem[0]
item.plex_UUID = emby_dbitem[0]
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
if item.plex_id is None:
item.uri = 'library://whatever/item/%s' % quote(item.file, safe='')
else:
item.uri = ('library://%s/item/library%%2Fmetadata%%2F%s' %
(item.plex_UUID, item.plex_id))
return item
def playlist_item_from_plex(plex_id):
"""
Returns a playlist element providing the plex_id ("ratingKey")
"""
item = Playlist_Item()
item.plex_id = plex_id
with embydb.GetEmbyDB() as emby_db:
emby_dbitem = emby_db.getItem_byId(plex_id)
try:
item.kodi_id = emby_dbitem[0]
item.kodi_type = emby_dbitem[4]
except:
raise KeyError('Could not find plex_id %s in database' % plex_id)
return item
def _log_xml(xml):
try:
xml.attrib
except AttributeError:
log.error('Did not receive an XML. Answer was: %s' % xml)
else:
from xml.etree.ElementTree import dump
log.error('XML received from the PMS: %s' % dump(xml))
def _get_playListVersion_from_xml(playlist, xml):
"""
Takes a PMS xml as input to overwrite the playlist version (e.g. Plex
playQueueVersion). Returns True if successful, False otherwise
"""
try:
playlist.version = int(xml.attrib['%sVersion' % playlist.kind])
except (TypeError, AttributeError, KeyError):
log.error('Could not get new playlist Version for playlist %s'
% playlist)
_log_xml(xml)
return False
return True
def _get_playlist_details_from_xml(playlist, xml):
"""
Takes a PMS xml as input and overwrites all the playlist's details, e.g.
playlist.ID with the XML's playQueueID
"""
try:
playlist.ID = xml.attrib['%sID' % playlist.kind]
playlist.version = xml.attrib['%sVersion' % playlist.kind]
playlist.selectedItemID = xml.attrib['%sSelectedItemID' % playlist.kind]
playlist.selectedItemOffset = xml.attrib['%sSelectedItemOffset' % playlist.kind]
playlist.shuffled = xml.attrib['%sShuffled' % playlist.kind]
except:
log.error('Could not parse xml answer from PMS for playlist %s'
% playlist)
import traceback
log.error(traceback.format_exc())
_log_xml(xml)
raise KeyError
def init_Plex_playlist(playlist, plex_id=None, kodi_item=None):
"""
Supply either plex_id or the data supplied by Kodi JSON-RPC
"""
if plex_id is not None:
item = playlist_item_from_plex(plex_id)
else:
item = playlist_item_from_kodi_item(kodi_item)
params = {
'next': 0,
'type': playlist.type,
'uri': item.uri
}
xml = DU().downloadUrl(url="{server}/%ss" % playlist.kind,
action_type="POST",
parameters=params)
_get_playlist_details_from_xml(xml)
playlist.items.append(item)
log.debug('Initialized the playlist: %s' % playlist)
def add_playlist_item(playlist, kodi_item, after_pos):
"""
Adds the new kodi_item to playlist after item at position after_pos
[int]
"""
item = playlist_item_from_kodi_item(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
xml = DU().downloadUrl(url, action_type="PUT")
try:
item.ID = xml.attrib['%sLastAddedItemID' % playlist.kind]
except (TypeError, AttributeError, KeyError):
log.error('Could not add item %s to playlist %s'
% (kodi_item, playlist))
_log_xml(xml)
return
playlist.items.append(item)
if after_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)
def move_playlist_item(playlist, before_pos, after_pos):
"""
Moves playlist item from before_pos [int] to after_pos [int]
"""
if after_pos == 0:
url = "{server}/%ss/%s/items/%s/move?after=0" % \
(playlist.kind,
playlist.ID,
playlist.items[before_pos].ID)
else:
url = "{server}/%ss/%s/items/%s/move?after=%s" % \
(playlist.kind,
playlist.ID,
playlist.items[before_pos].ID,
playlist.items[after_pos - 1].ID)
xml = DU().downloadUrl(url, action_type="PUT")
# We need to increment the playlistVersion
_get_playListVersion_from_xml(playlist, xml)
# Move our item's position in our internal playlist
playlist.items.insert(after_pos, playlist.items.pop(before_pos))
def delete_playlist_item(playlist, pos):
"""
Delete the item at position pos [int]
"""
xml = DU().downloadUrl("{server}/%ss/%s/items/%s?repeat=%s" %
(playlist.kind,
playlist.ID,
playlist.items[pos].ID,
playlist.repeat),
action_type="DELETE")
_get_playListVersion_from_xml(playlist, xml)
del playlist.items[pos]
def get_kodi_playlist_items(playlist):
"""
Returns a list of the current Kodi playlist items using JSON
E.g.:
[{u'title': u'3 Idiots', u'type': u'movie', u'id': 3, u'file':
u'smb://nas/PlexMovies/3 Idiots 2009 pt1.mkv', u'label': u'3 Idiots'}]
"""
answ = JSONRPC('Playlist.GetItems').execute({
'playlistid': playlist.playlistid,
'properties': ["title", "file"]
})
try:
answ = answ['result']['items']
except KeyError:
answ = []
return answ
def get_kodi_playqueues():
"""
Example return: [{u'playlistid': 0, u'type': u'audio'},
{u'playlistid': 1, u'type': u'video'},
{u'playlistid': 2, u'type': u'picture'}]
"""
queues = JSONRPC('Playlist.GetPlaylists').execute()
try:
queues = queues['result']
except KeyError:
raise KeyError('Could not get Kodi playqueues. JSON Result was: %s'
% queues)
return queues
# Functions operating on the Kodi playlist objects ##########
def insertintoPlaylist(self,
position,
dbid=None,
mediatype=None,
url=None):
params = {
'playlistid': self.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)
def addtoPlaylist(self, dbid=None, mediatype=None, url=None):
params = {
'playlistid': self.playlistId
}
if dbid is not None:
params['item'] = {'%sid' % tryEncode(mediatype): int(dbid)}
else:
params['item'] = {'file': url}
JSONRPC('Playlist.Add').execute(params)
def removefromPlaylist(self, position):
params = {
'playlistid': self.playlistId,
'position': position
}
JSONRPC('Playlist.Remove').execute(params)
def playAll(self, items, startitem, offset):
"""
items: list of dicts of the form
{
'playQueueItemID': Plex playQueueItemID, e.g. '29175'
'plexId': Plex ratingKey, e.g. '125'
'kodiId': Kodi's db id of the same item
}
startitem: tuple (typus, id), where typus is either
'playQueueItemID' or 'plexId' and id is the corresponding
id as a string
offset: First item's time offset to play in Kodi time (an int)
"""
log.info("---*** PLAY ALL ***---")
log.debug('Startitem: %s, offset: %s, items: %s'
% (startitem, offset, items))
self.items = items
if self.playlist is None:
self._initiatePlaylist()
if self.playlist is None:
log.error('Could not create playlist, abort')
return
window('plex_customplaylist', value="true")
if offset != 0:
# Seek to the starting position
window('plex_customplaylist.seektime', str(offset))
self._processItems(startitem, startPlayer=True)
# Log playlist
self._verifyPlaylist()
log.debug('Internal playlist: %s' % self.items)
def _processItems(self, startitem, startPlayer=False):
startpos = None
with embydb.GetEmbyDB() as emby_db:
for pos, item in enumerate(self.items):
kodiId = None
plexId = item['plexId']
embydb_item = emby_db.getItem_byId(plexId)
try:
kodiId = embydb_item[0]
mediatype = embydb_item[4]
except TypeError:
log.info('Couldnt find item %s in Kodi db' % plexId)
xml = PF.GetPlexMetadata(plexId)
if xml in (None, 401):
log.error('Could not download plexId %s' % plexId)
else:
log.debug('Downloaded xml metadata, adding now')
self._addtoPlaylist_xbmc(xml[0])
else:
# Add to playlist
log.debug("Adding %s PlexId %s, KodiId %s to playlist."
% (mediatype, plexId, kodiId))
self._addtoPlaylist(kodiId, mediatype)
# Add the kodiId
if kodiId is not None:
item['kodiId'] = str(kodiId)
if (startpos is None and startitem[1] == item[startitem[0]]):
startpos = pos
if startPlayer is True and len(self.playlist) > 0:
if startpos is not None:
self.player.play(self.playlist, startpos=startpos)
else:
log.info('Never received a starting item for playlist, '
'starting with the first entry')
self.player.play(self.playlist)
def _addtoPlaylist_xbmc(self, item):
API = PlexAPI.API(item)
params = {
'mode': "play",
'dbid': 'plextrailer',
'id': API.getRatingKey(),
'filename': API.getKey()
}
playurl = "plugin://plugin.video.plexkodiconnect.movies/?%s" \
% urlencode(params)
listitem = API.CreateListItemFromPlexItem()
playbackutils.PlaybackUtils(item).setArtwork(listitem)
self.playlist.add(playurl, listitem)
def clear(self):
"""
Empties current Kodi playlist and associated variables
"""
self.playlist.clear()
self.items = []
self.queueId = None
self.playQueueVersion = None
self.guid = None
log.info('Playlist cleared')

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

@ -0,0 +1,150 @@
# -*- coding: utf-8 -*-
###############################################################################
import logging
from threading import Lock, Thread
import xbmc
from utils import ThreadMethods, ThreadMethodsAdditionalSuspend, Lock_Function
import playlist_func as PL
from PlexFunctions import KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE, GetPlayQueue, \
ParseContainerKey
###############################################################################
log = logging.getLogger("PLEX."+__name__)
# Lock used to lock methods
lock = Lock()
lockmethod = Lock_Function(lock)
###############################################################################
@ThreadMethodsAdditionalSuspend('plex_serverStatus')
@ThreadMethods
class Playqueue(Thread):
"""
Monitors Kodi's playqueues for changes on the Kodi side
"""
# Borg - multiple instances, shared state
__shared_state = {}
playqueues = None
@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
# 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)
log.debug('Initialized the Kodi play queues: %s' % self.playqueues)
@lockmethod.lockthis
def update_playqueue_with_companion(self, data):
"""
Feed with Plex companion data
"""
# Get the correct queue
for playqueue in self.playqueues:
if playqueue.type == KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[
data['type']]:
break
@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
}
"""
for playqueue in self.playqueues:
if playqueue.playlistid == data['playlistid']:
break
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'])
@lockmethod.lockthis
def _compare_playqueues(self, playqueue, new):
"""
Used to poll the Kodi playqueue and update the Plex playqueue if needed
"""
old = playqueue.old_kodi_pl
log.debug('Comparing new Kodi playqueue %s with our play queue %s'
% (new, playqueue))
index = list(range(0, len(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']
else:
identical = (old_item['id'] == new_item['id'] and
old_item['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
PL.move_playlist_item(playqueue, i + j, i)
# Delete the item we just found
del old[i + j], index[i + j]
break
else:
# Did not find element i in the old list - Kodi monitor should
# pick this up!
# PL.add_playlist_item(playqueue, new_item, i-1)
pass
for i in index:
# Still got some old items left that need deleting
PL.delete_playlist_item(playqueue, i)
log.debug('New playqueue: %s' % playqueue)
def run(self):
threadStopped = self.threadStopped
threadSuspended = self.threadSuspended
log.info("----===## Starting PlayQueue client ##===----")
# Initialize the playqueues, if Kodi already got items in them
for playqueue in self.playqueues:
for i, item in enumerate(PL.get_kodi_playlist_items(playqueue)):
if i == 0:
PL.init_Plex_playlist(playqueue, kodi_item=item)
else:
PL.add_playlist_item(playqueue, item, i)
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)
log.info("----===## PlayQueue client stopped ##===----")

View file

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

View file

@ -133,21 +133,21 @@ def tryDecode(string, encoding='utf-8'):
def DateToKodi(stamp):
"""
converts a Unix time stamp (seconds passed sinceJanuary 1 1970) to a
propper, human-readable time stamp used by Kodi
"""
converts a Unix time stamp (seconds passed sinceJanuary 1 1970) to a
propper, human-readable time stamp used by Kodi
Output: Y-m-d h:m:s = 2009-04-05 23:16:04
Output: Y-m-d h:m:s = 2009-04-05 23:16:04
None if an error was encountered
"""
try:
stamp = float(stamp) + float(window('kodiplextimeoffset'))
date_time = time.localtime(stamp)
localdate = time.strftime('%Y-%m-%d %H:%M:%S', date_time)
except:
localdate = None
return localdate
None if an error was encountered
"""
try:
stamp = float(stamp) + float(window('kodiplextimeoffset'))
date_time = time.localtime(stamp)
localdate = time.strftime('%Y-%m-%d %H:%M:%S', date_time)
except:
localdate = None
return localdate
def IfExists(path):
@ -938,9 +938,33 @@ def ThreadMethods(cls):
return cls
class Lock_Function:
"""
Decorator for class methods and functions to lock them with lock.
Initialize this class first
lockfunction = Lock_Function(lock), where lock is a threading.Lock() object
To then lock a function or method:
@lockfunction.lockthis
def some_function(args, kwargs)
"""
def __init__(self, lock):
self.lock = lock
def lockthis(self, func):
@wraps(func)
def wrapper(*args, **kwargs):
with self.lock:
result = func(*args, **kwargs)
return result
return wrapper
###############################################################################
# UNUSED METHODS
def changePlayState(itemType, kodiId, playCount, lastplayed):
"""
YET UNUSED

View file

@ -5,6 +5,7 @@ import logging
import websocket
from json import loads
from threading import Thread
from Queue import Queue
from ssl import CERT_NONE
from xbmc import sleep
@ -24,10 +25,12 @@ log = logging.getLogger("PLEX."+__name__)
class WebSocket(Thread):
opcode_data = (websocket.ABNF.OPCODE_TEXT, websocket.ABNF.OPCODE_BINARY)
def __init__(self, queue):
def __init__(self, callback=None):
if callback is not None:
self.mgr = callback
self.ws = None
# Communication with librarysync
self.queue = queue
self.queue = Queue()
Thread.__init__(self)
def process(self, opcode, message):

View file

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