Playqueues major haul-over
This commit is contained in:
parent
95c87065ed
commit
0c2d4984ab
14 changed files with 673 additions and 678 deletions
|
@ -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)
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -19,7 +19,6 @@ import clientinfo
|
|||
import downloadutils
|
||||
import embydb_functions as embydb
|
||||
import playbackutils as pbutils
|
||||
import playlist
|
||||
|
||||
import PlexFunctions
|
||||
import PlexAPI
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -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 = []
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
394
resources/lib/playlist_func.py
Normal file
394
resources/lib/playlist_func.py
Normal 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
150
resources/lib/playqueue.py
Normal 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 ##===----")
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
95
service.py
95
service.py
|
@ -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:
|
||||
|
|
Loading…
Reference in a new issue