565 lines
19 KiB
Python
565 lines
19 KiB
Python
# -*- 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)
|