Merge branch 'develop'
This commit is contained in:
commit
2836f707c5
37 changed files with 2979 additions and 2077 deletions
|
@ -1,13 +1,11 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
<addon id="plugin.video.plexkodiconnect"
|
<addon id="plugin.video.plexkodiconnect"
|
||||||
name="PlexKodiConnect"
|
name="PlexKodiConnect"
|
||||||
version="1.5.1"
|
version="1.5.2"
|
||||||
provider-name="croneter">
|
provider-name="croneter">
|
||||||
<requires>
|
<requires>
|
||||||
<import addon="xbmc.python" version="2.1.0"/>
|
<import addon="xbmc.python" version="2.1.0"/>
|
||||||
<import addon="script.module.requests" version="2.3.0" />
|
<import addon="script.module.requests" version="2.3.0" />
|
||||||
<import addon="plugin.video.plexkodiconnect.movies" version="0.01" />
|
|
||||||
<import addon="plugin.video.plexkodiconnect.tvshows" version="0.01" />
|
|
||||||
</requires>
|
</requires>
|
||||||
<extension point="xbmc.python.pluginsource"
|
<extension point="xbmc.python.pluginsource"
|
||||||
library="default.py">
|
library="default.py">
|
||||||
|
|
|
@ -1,3 +1,14 @@
|
||||||
|
version 1.5.2 (beta only)
|
||||||
|
A DATABASE RESET IS ABSOLUTELY NECESSARY
|
||||||
|
- Plex Companion is completely rewired and should now handly anything you throw at it
|
||||||
|
- New playback startup mechanism for plugin paths
|
||||||
|
- Krypton: add ratings and IMDB id for movies
|
||||||
|
- Krypton: add ratings and theTvDB id for TV shows
|
||||||
|
- Don't support Plex Companion mirror
|
||||||
|
- Fix for Plex Companion not showing up
|
||||||
|
- Code rebranding from Emby to Plex, including a plex.db database :-)
|
||||||
|
- Lots of code refactoring and code optimizations
|
||||||
|
|
||||||
version 1.5.1 (beta only)
|
version 1.5.1 (beta only)
|
||||||
- Fix playstate and PMS item changes not working/not propagating anymore (caused by a change Plex made with the websocket interface). UPGRADE YOUR PMS!!
|
- Fix playstate and PMS item changes not working/not propagating anymore (caused by a change Plex made with the websocket interface). UPGRADE YOUR PMS!!
|
||||||
- Improvements to the way PKC behaves if the PMS goes offline
|
- Improvements to the way PKC behaves if the PMS goes offline
|
||||||
|
|
123
default.py
123
default.py
|
@ -3,36 +3,38 @@
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
from os import path as os_path
|
||||||
import sys
|
from sys import path as sys_path, argv
|
||||||
import urlparse
|
from urlparse import parse_qsl
|
||||||
|
|
||||||
import xbmc
|
from xbmc import translatePath, sleep, executebuiltin
|
||||||
import xbmcaddon
|
from xbmcaddon import Addon
|
||||||
import xbmcgui
|
from xbmcgui import ListItem, Dialog
|
||||||
|
from xbmcplugin import setResolvedUrl
|
||||||
|
|
||||||
|
_addon = Addon(id='plugin.video.plexkodiconnect')
|
||||||
_addon = xbmcaddon.Addon(id='plugin.video.plexkodiconnect')
|
|
||||||
try:
|
try:
|
||||||
_addon_path = _addon.getAddonInfo('path').decode('utf-8')
|
_addon_path = _addon.getAddonInfo('path').decode('utf-8')
|
||||||
except TypeError:
|
except TypeError:
|
||||||
_addon_path = _addon.getAddonInfo('path').decode()
|
_addon_path = _addon.getAddonInfo('path').decode()
|
||||||
try:
|
try:
|
||||||
_base_resource = xbmc.translatePath(os.path.join(
|
_base_resource = translatePath(os_path.join(
|
||||||
_addon_path,
|
_addon_path,
|
||||||
'resources',
|
'resources',
|
||||||
'lib')).decode('utf-8')
|
'lib')).decode('utf-8')
|
||||||
except TypeError:
|
except TypeError:
|
||||||
_base_resource = xbmc.translatePath(os.path.join(
|
_base_resource = translatePath(os_path.join(
|
||||||
_addon_path,
|
_addon_path,
|
||||||
'resources',
|
'resources',
|
||||||
'lib')).decode()
|
'lib')).decode()
|
||||||
sys.path.append(_base_resource)
|
sys_path.append(_base_resource)
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
import entrypoint
|
import entrypoint
|
||||||
import utils
|
from utils import window, pickl_window, reset, passwordsXML
|
||||||
|
from pickler import unpickle_me
|
||||||
|
from PKC_listitem import convert_PKC_to_listitem
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
@ -43,34 +45,47 @@ log = logging.getLogger("PLEX.default")
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
ARGV = argv
|
||||||
|
HANDLE = int(argv[1])
|
||||||
|
|
||||||
|
|
||||||
class Main():
|
class Main():
|
||||||
|
|
||||||
# MAIN ENTRY POINT
|
# MAIN ENTRY POINT
|
||||||
#@utils.profiling()
|
# @utils.profiling()
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
log.debug("Full sys.argv received: %s" % ARGV)
|
||||||
# Parse parameters
|
# Parse parameters
|
||||||
log.warn("Full sys.argv received: %s" % sys.argv)
|
params = dict(parse_qsl(ARGV[2][1:]))
|
||||||
base_url = sys.argv[0]
|
|
||||||
params = urlparse.parse_qs(sys.argv[2][1:])
|
|
||||||
try:
|
try:
|
||||||
mode = params['mode'][0]
|
mode = params['mode']
|
||||||
itemid = params.get('id', '')
|
itemid = params.get('id', '')
|
||||||
if itemid:
|
|
||||||
try:
|
|
||||||
itemid = itemid[0]
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
except:
|
except:
|
||||||
params = {}
|
|
||||||
mode = ""
|
mode = ""
|
||||||
|
itemid = ''
|
||||||
|
|
||||||
|
if mode == 'play':
|
||||||
|
# Put the request into the "queue"
|
||||||
|
while window('plex_play_new_item'):
|
||||||
|
sleep(20)
|
||||||
|
window('plex_play_new_item',
|
||||||
|
value='%s%s' % (mode, ARGV[2]))
|
||||||
|
# Wait for the result
|
||||||
|
while not pickl_window('plex_result'):
|
||||||
|
sleep(20)
|
||||||
|
result = unpickle_me()
|
||||||
|
if result is None:
|
||||||
|
log.error('Error encountered, aborting')
|
||||||
|
setResolvedUrl(HANDLE, False, ListItem())
|
||||||
|
elif result.listitem:
|
||||||
|
listitem = convert_PKC_to_listitem(result.listitem)
|
||||||
|
setResolvedUrl(HANDLE, True, listitem)
|
||||||
|
return
|
||||||
|
|
||||||
modes = {
|
modes = {
|
||||||
|
'reset': reset,
|
||||||
'reset': utils.reset,
|
|
||||||
'resetauth': entrypoint.resetAuth,
|
'resetauth': entrypoint.resetAuth,
|
||||||
'play': entrypoint.doPlayback,
|
'passwords': passwordsXML,
|
||||||
'passwords': utils.passwordsXML,
|
|
||||||
'channels': entrypoint.BrowseChannels,
|
'channels': entrypoint.BrowseChannels,
|
||||||
'channelsfolder': entrypoint.BrowseChannels,
|
'channelsfolder': entrypoint.BrowseChannels,
|
||||||
'browsecontent': entrypoint.BrowseContent,
|
'browsecontent': entrypoint.BrowseContent,
|
||||||
|
@ -79,7 +94,6 @@ class Main():
|
||||||
'inprogressepisodes': entrypoint.getInProgressEpisodes,
|
'inprogressepisodes': entrypoint.getInProgressEpisodes,
|
||||||
'recentepisodes': entrypoint.getRecentEpisodes,
|
'recentepisodes': entrypoint.getRecentEpisodes,
|
||||||
'refreshplaylist': entrypoint.refreshPlaylist,
|
'refreshplaylist': entrypoint.refreshPlaylist,
|
||||||
'companion': entrypoint.plexCompanion,
|
|
||||||
'switchuser': entrypoint.switchPlexUser,
|
'switchuser': entrypoint.switchPlexUser,
|
||||||
'deviceid': entrypoint.resetDeviceId,
|
'deviceid': entrypoint.resetDeviceId,
|
||||||
'delete': entrypoint.deleteItem,
|
'delete': entrypoint.deleteItem,
|
||||||
|
@ -89,24 +103,24 @@ class Main():
|
||||||
'watchlater': entrypoint.watchlater,
|
'watchlater': entrypoint.watchlater,
|
||||||
'enterPMS': entrypoint.enterPMS,
|
'enterPMS': entrypoint.enterPMS,
|
||||||
'togglePlexTV': entrypoint.togglePlexTV,
|
'togglePlexTV': entrypoint.togglePlexTV,
|
||||||
'playwatchlater': entrypoint.playWatchLater
|
'Plex_Node': entrypoint.Plex_Node
|
||||||
}
|
}
|
||||||
|
|
||||||
if "/extrafanart" in sys.argv[0]:
|
if "/extrafanart" in ARGV[0]:
|
||||||
plexpath = sys.argv[2][1:]
|
plexpath = ARGV[2][1:]
|
||||||
plexid = params.get('id', [""])[0]
|
plexid = params.get('id', [""])
|
||||||
entrypoint.getExtraFanArt(plexid, plexpath)
|
entrypoint.getExtraFanArt(plexid, plexpath)
|
||||||
entrypoint.getVideoFiles(plexid, plexpath)
|
entrypoint.getVideoFiles(plexid, plexpath)
|
||||||
return
|
return
|
||||||
|
|
||||||
if mode == 'fanart':
|
if mode == 'fanart':
|
||||||
log.info('User requested fanarttv refresh')
|
log.info('User requested fanarttv refresh')
|
||||||
utils.window('plex_runLibScan', value='fanart')
|
window('plex_runLibScan', value='fanart')
|
||||||
|
|
||||||
# Called by e.g. 3rd party plugin video extras
|
# Called by e.g. 3rd party plugin video extras
|
||||||
if ("/Extras" in sys.argv[0] or "/VideoFiles" in sys.argv[0] or
|
if ("/Extras" in ARGV[0] or "/VideoFiles" in ARGV[0] or
|
||||||
"/Extras" in sys.argv[2]):
|
"/Extras" in ARGV[2]):
|
||||||
plexId = params.get('id', [None])[0]
|
plexId = params.get('id', None)
|
||||||
entrypoint.getVideoFiles(plexId, params)
|
entrypoint.getVideoFiles(plexId, params)
|
||||||
|
|
||||||
if modes.get(mode):
|
if modes.get(mode):
|
||||||
|
@ -117,59 +131,60 @@ class Main():
|
||||||
modes[mode](itemid, dbid)
|
modes[mode](itemid, dbid)
|
||||||
|
|
||||||
elif mode in ("nextup", "inprogressepisodes"):
|
elif mode in ("nextup", "inprogressepisodes"):
|
||||||
limit = int(params['limit'][0])
|
limit = int(params['limit'])
|
||||||
modes[mode](itemid, limit)
|
modes[mode](itemid, limit)
|
||||||
|
|
||||||
elif mode in ("channels","getsubfolders"):
|
elif mode in ("channels","getsubfolders"):
|
||||||
modes[mode](itemid)
|
modes[mode](itemid)
|
||||||
|
|
||||||
elif mode == "browsecontent":
|
elif mode == "browsecontent":
|
||||||
modes[mode](itemid, params.get('type',[""])[0], params.get('folderid',[""])[0])
|
modes[mode](itemid, params.get('type',[""]), params.get('folderid',[""]))
|
||||||
|
|
||||||
elif mode == 'browseplex':
|
elif mode == 'browseplex':
|
||||||
modes[mode](
|
modes[mode](
|
||||||
itemid,
|
itemid,
|
||||||
params.get('type', [""])[0],
|
params.get('type', [""]),
|
||||||
params.get('folderid', [""])[0])
|
params.get('folderid', [""]))
|
||||||
|
|
||||||
elif mode in ('ondeck', 'recentepisodes'):
|
elif mode in ('ondeck', 'recentepisodes'):
|
||||||
modes[mode](
|
modes[mode](
|
||||||
itemid,
|
itemid,
|
||||||
params.get('type', [""])[0],
|
params.get('type', [""]),
|
||||||
params.get('tagname', [""])[0],
|
params.get('tagname', [""]),
|
||||||
int(params.get('limit', [""])[0]))
|
int(params.get('limit', [""])))
|
||||||
|
|
||||||
elif mode == "channelsfolder":
|
elif mode == "channelsfolder":
|
||||||
folderid = params['folderid'][0]
|
folderid = params['folderid']
|
||||||
modes[mode](itemid, folderid)
|
modes[mode](itemid, folderid)
|
||||||
elif mode == "companion":
|
elif mode == "companion":
|
||||||
modes[mode](itemid, params=sys.argv[2])
|
modes[mode](itemid, params=ARGV[2])
|
||||||
elif mode == 'playwatchlater':
|
elif mode == 'Plex_Node':
|
||||||
modes[mode](params.get('id')[0], params.get('viewOffset')[0])
|
modes[mode](params.get('id'),
|
||||||
|
params.get('viewOffset'),
|
||||||
|
params.get('plex_type'))
|
||||||
else:
|
else:
|
||||||
modes[mode]()
|
modes[mode]()
|
||||||
else:
|
else:
|
||||||
# Other functions
|
# Other functions
|
||||||
if mode == "settings":
|
if mode == "settings":
|
||||||
xbmc.executebuiltin('Addon.OpenSettings(plugin.video.plexkodiconnect)')
|
executebuiltin('Addon.OpenSettings(plugin.video.plexkodiconnect)')
|
||||||
elif mode in ("manualsync", "repair"):
|
elif mode in ("manualsync", "repair"):
|
||||||
if utils.window('plex_online') != "true":
|
if window('plex_online') != "true":
|
||||||
# Server is not online, do not run the sync
|
# Server is not online, do not run the sync
|
||||||
xbmcgui.Dialog().ok(
|
Dialog().ok(
|
||||||
"PlexKodiConnect",
|
"PlexKodiConnect",
|
||||||
"Unable to run the sync, the add-on is not connected "
|
"Unable to run the sync, the add-on is not connected "
|
||||||
"to a Plex server.")
|
"to a Plex server.")
|
||||||
log.error("Not connected to a PMS.")
|
log.error("Not connected to a PMS.")
|
||||||
else:
|
else:
|
||||||
if mode == 'repair':
|
if mode == 'repair':
|
||||||
utils.window('plex_runLibScan', value="repair")
|
window('plex_runLibScan', value="repair")
|
||||||
log.info("Requesting repair lib sync")
|
log.info("Requesting repair lib sync")
|
||||||
elif mode == 'manualsync':
|
elif mode == 'manualsync':
|
||||||
log.info("Requesting full library scan")
|
log.info("Requesting full library scan")
|
||||||
utils.window('plex_runLibScan', value="full")
|
window('plex_runLibScan', value="full")
|
||||||
|
|
||||||
elif mode == "texturecache":
|
elif mode == "texturecache":
|
||||||
utils.window('plex_runLibScan', value='del_textures')
|
window('plex_runLibScan', value='del_textures')
|
||||||
else:
|
else:
|
||||||
entrypoint.doMainListing()
|
entrypoint.doMainListing()
|
||||||
|
|
||||||
|
|
334
resources/lib/PKC_listitem.py
Normal file
334
resources/lib/PKC_listitem.py
Normal file
|
@ -0,0 +1,334 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
###############################################################################
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from xbmcgui import ListItem
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
log = logging.getLogger("PLEX."+__name__)
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
|
||||||
|
def convert_PKC_to_listitem(PKC_listitem):
|
||||||
|
"""
|
||||||
|
Insert a PKC_listitem and you will receive a valid XBMC listitem
|
||||||
|
"""
|
||||||
|
listitem = ListItem()
|
||||||
|
for func, args in PKC_listitem.data.items():
|
||||||
|
if isinstance(args, list):
|
||||||
|
for arg in args:
|
||||||
|
getattr(listitem, func)(*arg)
|
||||||
|
elif isinstance(args, dict):
|
||||||
|
for arg in args.items():
|
||||||
|
getattr(listitem, func)(*arg)
|
||||||
|
elif args is None:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
getattr(listitem, func)(args)
|
||||||
|
return listitem
|
||||||
|
|
||||||
|
|
||||||
|
class PKC_ListItem(object):
|
||||||
|
"""
|
||||||
|
Imitates xbmcgui.ListItem and its functions. Pass along PKC_Listitem().data
|
||||||
|
when pickling!
|
||||||
|
|
||||||
|
WARNING: set/get path only via setPath and getPath! (not getProperty)
|
||||||
|
"""
|
||||||
|
def __init__(self, label=None, label2=None, path=None):
|
||||||
|
self.data = {
|
||||||
|
'addStreamInfo': [], # (type, values: dict { label: value })
|
||||||
|
'setArt': [], # dict: { label: value }
|
||||||
|
'setInfo': {}, # type: infoLabel (dict { label: value })
|
||||||
|
'setLabel': label, # string
|
||||||
|
'setLabel2': label2, # string
|
||||||
|
'setPath': path, # string
|
||||||
|
'setProperty': {}, # (key, value)
|
||||||
|
'setSubtitles': [], # string
|
||||||
|
}
|
||||||
|
|
||||||
|
def addContextMenuItems(self, items, replaceItems):
|
||||||
|
"""
|
||||||
|
Adds item(s) to the context menu for media lists.
|
||||||
|
|
||||||
|
items : list - [(label, action,)*] A list of tuples consisting of label
|
||||||
|
and action pairs.
|
||||||
|
- label : string or unicode - item's label.
|
||||||
|
- action : string or unicode - any built-in function to perform.
|
||||||
|
replaceItes : [opt] bool - True=only your items will show/False=your
|
||||||
|
items will be amdded to context menu(Default).
|
||||||
|
|
||||||
|
List of functions - http://kodi.wiki/view/List_of_Built_In_Functions
|
||||||
|
|
||||||
|
*Note, You can use the above as keywords for arguments and skip
|
||||||
|
certain optional arguments.
|
||||||
|
|
||||||
|
Once you use a keyword, all following arguments require the keyword.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def addStreamInfo(self, type, values):
|
||||||
|
"""
|
||||||
|
Add a stream with details.
|
||||||
|
type : string - type of stream(video/audio/subtitle).
|
||||||
|
values : dictionary - pairs of { label: value }.
|
||||||
|
|
||||||
|
- Video Values:
|
||||||
|
- codec : string (h264)
|
||||||
|
- aspect : float (1.78)
|
||||||
|
- width : integer (1280)
|
||||||
|
- height : integer (720)
|
||||||
|
- duration : integer (seconds)
|
||||||
|
- Audio Values:
|
||||||
|
- codec : string (dts)
|
||||||
|
- language : string (en)
|
||||||
|
- channels : integer (2)
|
||||||
|
- Subtitle Values:
|
||||||
|
- language : string (en)
|
||||||
|
"""
|
||||||
|
self.data['addStreamInfo'].append((type, values))
|
||||||
|
|
||||||
|
def getLabel(self):
|
||||||
|
"""
|
||||||
|
Returns the listitem label
|
||||||
|
"""
|
||||||
|
return self.data['setLabel']
|
||||||
|
|
||||||
|
def getLabel2(self):
|
||||||
|
"""
|
||||||
|
Returns the listitem label.
|
||||||
|
"""
|
||||||
|
return self.data['setLabel2']
|
||||||
|
|
||||||
|
def getMusicInfoTag(self):
|
||||||
|
"""
|
||||||
|
returns the MusicInfoTag for this item.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def getProperty(self, key):
|
||||||
|
"""
|
||||||
|
Returns a listitem property as a string, similar to an infolabel.
|
||||||
|
key : string - property name.
|
||||||
|
*Note, Key is NOT case sensitive.
|
||||||
|
|
||||||
|
You can use the above as keywords for arguments and skip certain
|
||||||
|
optional arguments.
|
||||||
|
|
||||||
|
Once you use a keyword, all following arguments require the keyword.
|
||||||
|
"""
|
||||||
|
return self.data['setProperty'].get(key)
|
||||||
|
|
||||||
|
def getVideoInfoTag(self):
|
||||||
|
"""
|
||||||
|
returns the VideoInfoTag for this item
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def getdescription(self):
|
||||||
|
"""
|
||||||
|
Returns the description of this PlayListItem
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def getduration(self):
|
||||||
|
"""
|
||||||
|
Returns the duration of this PlayListItem
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def getfilename(self):
|
||||||
|
"""
|
||||||
|
Returns the filename of this PlayListItem.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def isSelected(self):
|
||||||
|
"""
|
||||||
|
Returns the listitem's selected status
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def select(self):
|
||||||
|
"""
|
||||||
|
Sets the listitem's selected status.
|
||||||
|
selected : bool - True=selected/False=not selected
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def setArt(self, values):
|
||||||
|
"""
|
||||||
|
Sets the listitem's art
|
||||||
|
values : dictionary - pairs of { label: value }.
|
||||||
|
|
||||||
|
Some default art values (any string possible):
|
||||||
|
- thumb : string - image filename
|
||||||
|
- poster : string - image filename
|
||||||
|
- banner : string - image filename
|
||||||
|
- fanart : string - image filename
|
||||||
|
- clearart : string - image filename
|
||||||
|
- clearlogo : string - image filename
|
||||||
|
- landscape : string - image filename
|
||||||
|
- icon : string - image filename
|
||||||
|
"""
|
||||||
|
self.data['setArt'].append(values)
|
||||||
|
|
||||||
|
def setContentLookup(self, enable):
|
||||||
|
"""
|
||||||
|
Enable or disable content lookup for item.
|
||||||
|
|
||||||
|
If disabled, HEAD requests to e.g determine mime type will not be sent.
|
||||||
|
|
||||||
|
enable : bool
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def setInfo(self, type, infoLabels):
|
||||||
|
"""
|
||||||
|
type : string - type of media(video/music/pictures).
|
||||||
|
|
||||||
|
infoLabels : dictionary - pairs of { label: value }. *Note, To set
|
||||||
|
pictures exif info, prepend 'exif:' to the label. Exif values must be
|
||||||
|
passed as strings, separate value pairs with a comma. (eg.
|
||||||
|
{'exif:resolution': '720,480'}
|
||||||
|
|
||||||
|
See CPictureInfoTag::TranslateString in PictureInfoTag.cpp for valid
|
||||||
|
strings. You can use the above as keywords for arguments and skip
|
||||||
|
certain optional arguments.
|
||||||
|
|
||||||
|
Once you use a keyword, all following arguments require the keyword.
|
||||||
|
|
||||||
|
- General Values that apply to all types:
|
||||||
|
- count : integer (12) - can be used to store an id for later, or
|
||||||
|
for sorting purposes
|
||||||
|
- size : long (1024) - size in bytes
|
||||||
|
- date : string (d.m.Y / 01.01.2009) - file date
|
||||||
|
|
||||||
|
- Video Values:
|
||||||
|
- genre : string (Comedy)
|
||||||
|
- year : integer (2009)
|
||||||
|
- episode : integer (4)
|
||||||
|
- season : integer (1)
|
||||||
|
- top250 : integer (192)
|
||||||
|
- tracknumber : integer (3)
|
||||||
|
- rating : float (6.4) - range is 0..10
|
||||||
|
- userrating : integer (9) - range is 1..10
|
||||||
|
- watched : depreciated - use playcount instead
|
||||||
|
- playcount : integer (2) - number of times this item has been
|
||||||
|
played
|
||||||
|
- overlay : integer (2) - range is 0..8. See GUIListItem.h for
|
||||||
|
values
|
||||||
|
- cast : list (["Michal C. Hall","Jennifer Carpenter"]) - if
|
||||||
|
provided a list of tuples cast will be interpreted as castandrole
|
||||||
|
- castandrole : list of tuples ([("Michael C.
|
||||||
|
Hall","Dexter"),("Jennifer Carpenter","Debra")])
|
||||||
|
- director : string (Dagur Kari)
|
||||||
|
- mpaa : string (PG-13)
|
||||||
|
- plot : string (Long Description)
|
||||||
|
- plotoutline : string (Short Description)
|
||||||
|
- title : string (Big Fan)
|
||||||
|
- originaltitle : string (Big Fan)
|
||||||
|
- sorttitle : string (Big Fan)
|
||||||
|
- duration : integer (245) - duration in seconds
|
||||||
|
- studio : string (Warner Bros.)
|
||||||
|
- tagline : string (An awesome movie) - short description of movie
|
||||||
|
- writer : string (Robert D. Siegel)
|
||||||
|
- tvshowtitle : string (Heroes)
|
||||||
|
- premiered : string (2005-03-04)
|
||||||
|
- status : string (Continuing) - status of a TVshow
|
||||||
|
- code : string (tt0110293) - IMDb code
|
||||||
|
- aired : string (2008-12-07)
|
||||||
|
- credits : string (Andy Kaufman) - writing credits
|
||||||
|
- lastplayed : string (Y-m-d h:m:s = 2009-04-05 23:16:04)
|
||||||
|
- album : string (The Joshua Tree)
|
||||||
|
- artist : list (['U2'])
|
||||||
|
- votes : string (12345 votes)
|
||||||
|
- trailer : string (/home/user/trailer.avi)
|
||||||
|
- dateadded : string (Y-m-d h:m:s = 2009-04-05 23:16:04)
|
||||||
|
- mediatype : string - "video", "movie", "tvshow", "season",
|
||||||
|
"episode" or "musicvideo"
|
||||||
|
|
||||||
|
- Music Values:
|
||||||
|
- tracknumber : integer (8)
|
||||||
|
- discnumber : integer (2)
|
||||||
|
- duration : integer (245) - duration in seconds
|
||||||
|
- year : integer (1998)
|
||||||
|
- genre : string (Rock)
|
||||||
|
- album : string (Pulse)
|
||||||
|
- artist : string (Muse)
|
||||||
|
- title : string (American Pie)
|
||||||
|
- rating : string (3) - single character between 0 and 5
|
||||||
|
- lyrics : string (On a dark desert highway...)
|
||||||
|
- playcount : integer (2) - number of times this item has been
|
||||||
|
played
|
||||||
|
- lastplayed : string (Y-m-d h:m:s = 2009-04-05 23:16:04)
|
||||||
|
|
||||||
|
- Picture Values:
|
||||||
|
- title : string (In the last summer-1)
|
||||||
|
- picturepath : string (/home/username/pictures/img001.jpg)
|
||||||
|
- exif : string (See CPictureInfoTag::TranslateString in
|
||||||
|
PictureInfoTag.cpp for valid strings)
|
||||||
|
"""
|
||||||
|
self.data['setInfo'][type] = infoLabels
|
||||||
|
|
||||||
|
def setLabel(self, label):
|
||||||
|
"""
|
||||||
|
Sets the listitem's label.
|
||||||
|
label : string or unicode - text string.
|
||||||
|
"""
|
||||||
|
self.data['setLabel'] = label
|
||||||
|
|
||||||
|
def setLabel2(self, label):
|
||||||
|
"""
|
||||||
|
Sets the listitem's label2.
|
||||||
|
label : string or unicode - text string.
|
||||||
|
"""
|
||||||
|
self.data['setLabel2'] = label
|
||||||
|
|
||||||
|
def setMimeType(self, mimetype):
|
||||||
|
"""
|
||||||
|
Sets the listitem's mimetype if known.
|
||||||
|
mimetype : string or unicode - mimetype.
|
||||||
|
|
||||||
|
If known prehand, this can (but does not have to) avoid HEAD requests
|
||||||
|
being sent to HTTP servers to figure out file type.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def setPath(self, path):
|
||||||
|
"""
|
||||||
|
Sets the listitem's path.
|
||||||
|
path : string or unicode - path, activated when item is clicked.
|
||||||
|
|
||||||
|
*Note, You can use the above as keywords for arguments.
|
||||||
|
"""
|
||||||
|
self.data['setPath'] = path
|
||||||
|
|
||||||
|
def setProperty(self, key, value):
|
||||||
|
"""
|
||||||
|
Sets a listitem property, similar to an infolabel.
|
||||||
|
key : string - property name.
|
||||||
|
value : string or unicode - value of property.
|
||||||
|
*Note, Key is NOT case sensitive.
|
||||||
|
|
||||||
|
You can use the above as keywords for arguments and skip certain
|
||||||
|
optional arguments. Once you use a keyword, all following arguments
|
||||||
|
require the keyword.
|
||||||
|
|
||||||
|
Some of these are treated internally by XBMC, such as the
|
||||||
|
'StartOffset' property, which is the offset in seconds at which to
|
||||||
|
start playback of an item. Others may be used in the skin to add extra
|
||||||
|
information, such as 'WatchedCount' for tvshow items
|
||||||
|
"""
|
||||||
|
self.data['setProperty'][key] = value
|
||||||
|
|
||||||
|
def setSubtitles(self, subtitles):
|
||||||
|
"""
|
||||||
|
Sets subtitles for this listitem. Pass in a list of filepaths
|
||||||
|
|
||||||
|
example:
|
||||||
|
- listitem.setSubtitles(['special://temp/example.srt',
|
||||||
|
'http://example.com/example.srt' ])
|
||||||
|
"""
|
||||||
|
self.data['setSubtitles'].extend(([subtitles],))
|
|
@ -50,8 +50,9 @@ import downloadutils
|
||||||
from utils import window, settings, language as lang, tryDecode, tryEncode, \
|
from utils import window, settings, language as lang, tryDecode, tryEncode, \
|
||||||
DateToKodi, KODILANGUAGE
|
DateToKodi, KODILANGUAGE
|
||||||
from PlexFunctions import PLEX_TO_KODI_TIMEFACTOR, PMSHttpsEnabled, \
|
from PlexFunctions import PLEX_TO_KODI_TIMEFACTOR, PMSHttpsEnabled, \
|
||||||
REMAP_TYPE_FROM_PLEXTYPE
|
REMAP_TYPE_FROM_PLEXTYPE, PLEX_TYPE_MOVIE, PLEX_TYPE_SHOW, \
|
||||||
import embydb_functions as embydb
|
PLEX_TYPE_EPISODE
|
||||||
|
import plexdb_functions as plexdb
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
@ -1646,7 +1647,7 @@ class API():
|
||||||
|
|
||||||
If not found, empty str is returned
|
If not found, empty str is returned
|
||||||
"""
|
"""
|
||||||
return self.item.attrib.get('playQueueItemID', '')
|
return self.item.attrib.get('playQueueItemID')
|
||||||
|
|
||||||
def getDataFromPartOrMedia(self, key):
|
def getDataFromPartOrMedia(self, key):
|
||||||
"""
|
"""
|
||||||
|
@ -1915,9 +1916,9 @@ class API():
|
||||||
# Return the saved Plex id's, if applicable
|
# Return the saved Plex id's, if applicable
|
||||||
# Always seek collection's ids since not provided by PMS
|
# Always seek collection's ids since not provided by PMS
|
||||||
if collection is False:
|
if collection is False:
|
||||||
if media_type == 'movie':
|
if media_type == PLEX_TYPE_MOVIE:
|
||||||
mediaId = self.getProvider('imdb')
|
mediaId = self.getProvider('imdb')
|
||||||
elif media_type == 'show':
|
elif media_type == PLEX_TYPE_SHOW:
|
||||||
mediaId = self.getProvider('tvdb')
|
mediaId = self.getProvider('tvdb')
|
||||||
if mediaId is not None:
|
if mediaId is not None:
|
||||||
return mediaId
|
return mediaId
|
||||||
|
@ -1927,7 +1928,7 @@ class API():
|
||||||
log.info('Start movie set/collection lookup on themoviedb')
|
log.info('Start movie set/collection lookup on themoviedb')
|
||||||
|
|
||||||
apiKey = settings('themoviedbAPIKey')
|
apiKey = settings('themoviedbAPIKey')
|
||||||
if media_type == 'show':
|
if media_type == PLEX_TYPE_SHOW:
|
||||||
media_type = 'tv'
|
media_type = 'tv'
|
||||||
title = item.get('title', '')
|
title = item.get('title', '')
|
||||||
# if the title has the year in remove it as tmdb cannot deal with it...
|
# if the title has the year in remove it as tmdb cannot deal with it...
|
||||||
|
@ -2305,10 +2306,10 @@ class API():
|
||||||
kodiindex = 0
|
kodiindex = 0
|
||||||
for stream in mediastreams:
|
for stream in mediastreams:
|
||||||
index = stream.attrib['id']
|
index = stream.attrib['id']
|
||||||
# Since Emby returns all possible tracks together, have to pull
|
# Since plex returns all possible tracks together, have to pull
|
||||||
# only external subtitles.
|
# only external subtitles.
|
||||||
key = stream.attrib.get('key')
|
key = stream.attrib.get('key')
|
||||||
# IsTextSubtitleStream if true, is available to download from emby.
|
# IsTextSubtitleStream if true, is available to download from plex.
|
||||||
if stream.attrib.get('streamType') == "3" and key:
|
if stream.attrib.get('streamType') == "3" and key:
|
||||||
# Direct stream
|
# Direct stream
|
||||||
url = ("%s%s" % (self.server, key))
|
url = ("%s%s" % (self.server, key))
|
||||||
|
@ -2318,7 +2319,7 @@ class API():
|
||||||
externalsubs.append(url)
|
externalsubs.append(url)
|
||||||
kodiindex += 1
|
kodiindex += 1
|
||||||
mapping = json.dumps(mapping)
|
mapping = json.dumps(mapping)
|
||||||
window('emby_%s.indexMapping' % playurl, value=mapping)
|
window('plex_%s.indexMapping' % playurl, value=mapping)
|
||||||
log.info('Found external subs: %s' % externalsubs)
|
log.info('Found external subs: %s' % externalsubs)
|
||||||
return externalsubs
|
return externalsubs
|
||||||
|
|
||||||
|
@ -2393,7 +2394,7 @@ class API():
|
||||||
# listItem.setProperty('isPlayable', 'true')
|
# listItem.setProperty('isPlayable', 'true')
|
||||||
# listItem.setProperty('isFolder', 'true')
|
# listItem.setProperty('isFolder', 'true')
|
||||||
# Further stuff
|
# Further stuff
|
||||||
listItem.setIconImage('DefaultPicture.png')
|
listItem.setArt({'icon': 'DefaultPicture.png'})
|
||||||
return listItem
|
return listItem
|
||||||
|
|
||||||
def _createVideoListItem(self,
|
def _createVideoListItem(self,
|
||||||
|
@ -2456,21 +2457,21 @@ class API():
|
||||||
"s%.2de%.2d" % (season, episode))
|
"s%.2de%.2d" % (season, episode))
|
||||||
if appendSxxExx is True:
|
if appendSxxExx is True:
|
||||||
title = "S%.2dE%.2d - %s" % (season, episode, title)
|
title = "S%.2dE%.2d - %s" % (season, episode, title)
|
||||||
listItem.setIconImage('DefaultTVShows.png')
|
listItem.setArt({'icon': 'DefaultTVShows.png'})
|
||||||
if appendShowTitle is True:
|
if appendShowTitle is True:
|
||||||
title = "%s - %s " % (show, title)
|
title = "%s - %s " % (show, title)
|
||||||
elif typus == "movie":
|
elif typus == "movie":
|
||||||
listItem.setIconImage('DefaultMovies.png')
|
listItem.setArt({'icon': 'DefaultMovies.png'})
|
||||||
else:
|
else:
|
||||||
# E.g. clips, trailers, ...
|
# E.g. clips, trailers, ...
|
||||||
listItem.setIconImage('DefaultVideo.png')
|
listItem.setArt({'icon': 'DefaultVideo.png'})
|
||||||
|
|
||||||
plexId = self.getRatingKey()
|
plexId = self.getRatingKey()
|
||||||
listItem.setProperty('plexid', plexId)
|
listItem.setProperty('plexid', plexId)
|
||||||
with embydb.GetEmbyDB() as emby_db:
|
with plexdb.Get_Plex_DB() as plex_db:
|
||||||
try:
|
try:
|
||||||
listItem.setProperty('dbid',
|
listItem.setProperty('dbid',
|
||||||
str(emby_db.getItem_byId(plexId)[0]))
|
str(plex_db.getItem_byId(plexId)[0]))
|
||||||
except TypeError:
|
except TypeError:
|
||||||
pass
|
pass
|
||||||
# Expensive operation
|
# Expensive operation
|
||||||
|
@ -2563,3 +2564,68 @@ class API():
|
||||||
line1=lang(39031) + url,
|
line1=lang(39031) + url,
|
||||||
line2=lang(39032))
|
line2=lang(39032))
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
def set_listitem_artwork(self, listitem):
|
||||||
|
"""
|
||||||
|
Set all artwork to the listitem
|
||||||
|
"""
|
||||||
|
allartwork = self.getAllArtwork(parentInfo=True)
|
||||||
|
arttypes = {
|
||||||
|
'poster': "Primary",
|
||||||
|
'tvshow.poster': "Thumb",
|
||||||
|
'clearart': "Art",
|
||||||
|
'tvshow.clearart': "Art",
|
||||||
|
'clearart': "Primary",
|
||||||
|
'tvshow.clearart': "Primary",
|
||||||
|
'clearlogo': "Logo",
|
||||||
|
'tvshow.clearlogo': "Logo",
|
||||||
|
'discart': "Disc",
|
||||||
|
'fanart_image': "Backdrop",
|
||||||
|
'landscape': "Backdrop",
|
||||||
|
"banner": "Banner"
|
||||||
|
}
|
||||||
|
for arttype in arttypes:
|
||||||
|
art = arttypes[arttype]
|
||||||
|
if art == "Backdrop":
|
||||||
|
try:
|
||||||
|
# Backdrop is a list, grab the first backdrop
|
||||||
|
self._set_listitem_artprop(listitem,
|
||||||
|
arttype,
|
||||||
|
allartwork[art][0])
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
self._set_listitem_artprop(listitem, arttype, allartwork[art])
|
||||||
|
|
||||||
|
def _set_listitem_artprop(self, listitem, arttype, path):
|
||||||
|
if arttype in (
|
||||||
|
'thumb', 'fanart_image', 'small_poster', 'tiny_poster',
|
||||||
|
'medium_landscape', 'medium_poster', 'small_fanartimage',
|
||||||
|
'medium_fanartimage', 'fanart_noindicators'):
|
||||||
|
listitem.setProperty(arttype, path)
|
||||||
|
else:
|
||||||
|
listitem.setArt({arttype: path})
|
||||||
|
|
||||||
|
def set_playback_win_props(self, playurl, listitem):
|
||||||
|
"""
|
||||||
|
Set all properties necessary for plugin path playback for listitem
|
||||||
|
"""
|
||||||
|
itemtype = self.getType()
|
||||||
|
userdata = self.getUserData()
|
||||||
|
|
||||||
|
plexitem = "plex_%s" % playurl
|
||||||
|
window('%s.runtime' % plexitem, value=str(userdata['Runtime']))
|
||||||
|
window('%s.type' % plexitem, value=itemtype)
|
||||||
|
window('%s.itemid' % plexitem, value=self.getRatingKey())
|
||||||
|
window('%s.playcount' % plexitem, value=str(userdata['PlayCount']))
|
||||||
|
|
||||||
|
if itemtype == PLEX_TYPE_EPISODE:
|
||||||
|
window('%s.refreshid' % plexitem, value=self.getParentRatingKey())
|
||||||
|
else:
|
||||||
|
window('%s.refreshid' % plexitem, value=self.getRatingKey())
|
||||||
|
|
||||||
|
# Append external subtitles to stream
|
||||||
|
playmethod = window('%s.playmethod' % plexitem)
|
||||||
|
if playmethod in ("DirectStream", "DirectPlay"):
|
||||||
|
subtitles = self.externalSubs(playurl)
|
||||||
|
listitem.setSubtitles(subtitles)
|
||||||
|
|
|
@ -1,19 +1,17 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import logging
|
import logging
|
||||||
import threading
|
from threading import Thread
|
||||||
import traceback
|
|
||||||
import socket
|
|
||||||
import Queue
|
import Queue
|
||||||
|
from socket import SHUT_RDWR
|
||||||
|
|
||||||
import xbmc
|
from xbmc import sleep
|
||||||
|
|
||||||
from utils import settings, ThreadMethodsAdditionalSuspend, ThreadMethods
|
from utils import settings, ThreadMethodsAdditionalSuspend, ThreadMethods
|
||||||
from plexbmchelper import listener, plexgdm, subscribers, functions, \
|
from plexbmchelper import listener, plexgdm, subscribers, functions, \
|
||||||
httppersist, plexsettings
|
httppersist, plexsettings
|
||||||
from PlexFunctions import ParseContainerKey, GetPlayQueue, \
|
from PlexFunctions import ParseContainerKey, KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE
|
||||||
ConvertPlexToKodiTime
|
|
||||||
import playlist
|
|
||||||
import player
|
import player
|
||||||
|
from entrypoint import Plex_Node
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
@ -24,24 +22,23 @@ log = logging.getLogger("PLEX."+__name__)
|
||||||
|
|
||||||
@ThreadMethodsAdditionalSuspend('plex_serverStatus')
|
@ThreadMethodsAdditionalSuspend('plex_serverStatus')
|
||||||
@ThreadMethods
|
@ThreadMethods
|
||||||
class PlexCompanion(threading.Thread):
|
class PlexCompanion(Thread):
|
||||||
"""
|
"""
|
||||||
Initialize with a Queue for callbacks
|
|
||||||
"""
|
"""
|
||||||
def __init__(self):
|
def __init__(self, callback=None):
|
||||||
log.info("----===## Starting PlexCompanion ##===----")
|
log.info("----===## Starting PlexCompanion ##===----")
|
||||||
|
if callback is not None:
|
||||||
|
self.mgr = callback
|
||||||
self.settings = plexsettings.getSettings()
|
self.settings = plexsettings.getSettings()
|
||||||
# Start GDM for server/client discovery
|
# Start GDM for server/client discovery
|
||||||
self.client = plexgdm.plexgdm()
|
self.client = plexgdm.plexgdm()
|
||||||
self.client.clientDetails(self.settings)
|
self.client.clientDetails(self.settings)
|
||||||
log.debug("Registration string is: %s "
|
log.debug("Registration string is: %s "
|
||||||
% self.client.getClientDetails())
|
% self.client.getClientDetails())
|
||||||
# Initialize playlist/queue stuff
|
|
||||||
self.playlist = playlist.Playlist('video')
|
|
||||||
# kodi player instance
|
# kodi player instance
|
||||||
self.player = player.Player()
|
self.player = player.Player()
|
||||||
|
|
||||||
threading.Thread.__init__(self)
|
Thread.__init__(self)
|
||||||
|
|
||||||
def _getStartItem(self, string):
|
def _getStartItem(self, string):
|
||||||
"""
|
"""
|
||||||
|
@ -62,62 +59,48 @@ class PlexCompanion(threading.Thread):
|
||||||
|
|
||||||
def processTasks(self, task):
|
def processTasks(self, task):
|
||||||
"""
|
"""
|
||||||
Processes tasks picked up e.g. by Companion listener
|
Processes tasks picked up e.g. by Companion listener, e.g.
|
||||||
|
{'action': 'playlist',
|
||||||
task = {
|
'data': {'address': 'xyz.plex.direct',
|
||||||
'action': 'playlist'
|
'commandID': '7',
|
||||||
'data': as received from Plex companion
|
'containerKey': '/playQueues/6669?own=1&repeat=0&window=200',
|
||||||
}
|
'key': '/library/metadata/220493',
|
||||||
|
'machineIdentifier': 'xyz',
|
||||||
|
'offset': '0',
|
||||||
|
'port': '32400',
|
||||||
|
'protocol': 'https',
|
||||||
|
'token': 'transient-cd2527d1-0484-48e0-a5f7-f5caa7d591bd',
|
||||||
|
'type': 'video'}}
|
||||||
"""
|
"""
|
||||||
log.debug('Processing: %s' % task)
|
log.debug('Processing: %s' % task)
|
||||||
data = task['data']
|
data = task['data']
|
||||||
|
|
||||||
if task['action'] == 'playlist':
|
if (task['action'] == 'playlist' and
|
||||||
|
data.get('address') == 'node.plexapp.com'):
|
||||||
|
# E.g. watch later initiated by Companion
|
||||||
|
thread = Thread(target=Plex_Node,
|
||||||
|
args=('{server}%s' % data.get('key'),
|
||||||
|
data.get('offset'),
|
||||||
|
data.get('type'),
|
||||||
|
True),)
|
||||||
|
thread.setDaemon(True)
|
||||||
|
thread.start()
|
||||||
|
elif task['action'] == 'playlist':
|
||||||
|
# Get the playqueue ID
|
||||||
try:
|
try:
|
||||||
_, queueId, query = ParseContainerKey(data['containerKey'])
|
_, ID, query = ParseContainerKey(data['containerKey'])
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error('Exception while processing: %s' % e)
|
log.error('Exception while processing: %s' % e)
|
||||||
import traceback
|
import traceback
|
||||||
log.error("Traceback:\n%s" % traceback.format_exc())
|
log.error("Traceback:\n%s" % traceback.format_exc())
|
||||||
return
|
return
|
||||||
if self.playlist is not None:
|
playqueue = self.mgr.playqueue.get_playqueue_from_type(
|
||||||
if self.playlist.Typus() != data.get('type'):
|
KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[data['type']])
|
||||||
log.debug('Switching to Kodi playlist of type %s'
|
self.mgr.playqueue.update_playqueue_from_PMS(
|
||||||
% data.get('type'))
|
playqueue,
|
||||||
self.playlist = None
|
ID,
|
||||||
if self.playlist is None:
|
repeat=query.get('repeat'),
|
||||||
if data.get('type') == 'music':
|
offset=data.get('offset'))
|
||||||
self.playlist = playlist.Playlist('music')
|
|
||||||
else:
|
|
||||||
self.playlist = playlist.Playlist('video')
|
|
||||||
if queueId != self.playlist.QueueId():
|
|
||||||
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()
|
|
||||||
# Set new values
|
|
||||||
self.playlist.QueueId(queueId)
|
|
||||||
self.playlist.PlayQueueVersion(int(
|
|
||||||
xml.attrib.get('playQueueVersion')))
|
|
||||||
self.playlist.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(
|
|
||||||
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()))
|
|
||||||
else:
|
|
||||||
log.error('This has never happened before!')
|
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
httpd = False
|
httpd = False
|
||||||
|
@ -130,7 +113,7 @@ class PlexCompanion(threading.Thread):
|
||||||
requestMgr = httppersist.RequestMgr()
|
requestMgr = httppersist.RequestMgr()
|
||||||
jsonClass = functions.jsonClass(requestMgr, self.settings)
|
jsonClass = functions.jsonClass(requestMgr, self.settings)
|
||||||
subscriptionManager = subscribers.SubscriptionManager(
|
subscriptionManager = subscribers.SubscriptionManager(
|
||||||
jsonClass, requestMgr, self.player, self.playlist)
|
jsonClass, requestMgr, self.player, self.mgr)
|
||||||
|
|
||||||
queue = Queue.Queue(maxsize=100)
|
queue = Queue.Queue(maxsize=100)
|
||||||
|
|
||||||
|
@ -151,9 +134,10 @@ class PlexCompanion(threading.Thread):
|
||||||
break
|
break
|
||||||
except:
|
except:
|
||||||
log.error("Unable to start PlexCompanion. Traceback:")
|
log.error("Unable to start PlexCompanion. Traceback:")
|
||||||
|
import traceback
|
||||||
log.error(traceback.print_exc())
|
log.error(traceback.print_exc())
|
||||||
|
|
||||||
xbmc.sleep(3000)
|
sleep(3000)
|
||||||
|
|
||||||
if start_count == 3:
|
if start_count == 3:
|
||||||
log.error("Error: Unable to start web helper.")
|
log.error("Error: Unable to start web helper.")
|
||||||
|
@ -168,7 +152,7 @@ class PlexCompanion(threading.Thread):
|
||||||
|
|
||||||
message_count = 0
|
message_count = 0
|
||||||
if httpd:
|
if httpd:
|
||||||
t = threading.Thread(target=httpd.handle_request)
|
t = Thread(target=httpd.handle_request)
|
||||||
|
|
||||||
while not threadStopped():
|
while not threadStopped():
|
||||||
# If we are not authorized, sleep
|
# If we are not authorized, sleep
|
||||||
|
@ -177,13 +161,13 @@ class PlexCompanion(threading.Thread):
|
||||||
while threadSuspended():
|
while threadSuspended():
|
||||||
if threadStopped():
|
if threadStopped():
|
||||||
break
|
break
|
||||||
xbmc.sleep(1000)
|
sleep(1000)
|
||||||
try:
|
try:
|
||||||
message_count += 1
|
message_count += 1
|
||||||
if httpd:
|
if httpd:
|
||||||
if not t.isAlive():
|
if not t.isAlive():
|
||||||
# Use threads cause the method will stall
|
# Use threads cause the method will stall
|
||||||
t = threading.Thread(target=httpd.handle_request)
|
t = Thread(target=httpd.handle_request)
|
||||||
t.start()
|
t.start()
|
||||||
|
|
||||||
if message_count == 3000:
|
if message_count == 3000:
|
||||||
|
@ -202,6 +186,7 @@ class PlexCompanion(threading.Thread):
|
||||||
message_count = 0
|
message_count = 0
|
||||||
except:
|
except:
|
||||||
log.warn("Error in loop, continuing anyway. Traceback:")
|
log.warn("Error in loop, continuing anyway. Traceback:")
|
||||||
|
import traceback
|
||||||
log.warn(traceback.format_exc())
|
log.warn(traceback.format_exc())
|
||||||
# See if there's anything we need to process
|
# See if there's anything we need to process
|
||||||
try:
|
try:
|
||||||
|
@ -214,12 +199,12 @@ class PlexCompanion(threading.Thread):
|
||||||
queue.task_done()
|
queue.task_done()
|
||||||
# Don't sleep
|
# Don't sleep
|
||||||
continue
|
continue
|
||||||
xbmc.sleep(20)
|
sleep(20)
|
||||||
|
|
||||||
client.stop_all()
|
client.stop_all()
|
||||||
if httpd:
|
if httpd:
|
||||||
try:
|
try:
|
||||||
httpd.socket.shutdown(socket.SHUT_RDWR)
|
httpd.socket.shutdown(SHUT_RDWR)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
finally:
|
finally:
|
||||||
|
|
|
@ -20,46 +20,118 @@ addonName = 'PlexKodiConnect'
|
||||||
# Multiply Plex time by this factor to receive Kodi time
|
# Multiply Plex time by this factor to receive Kodi time
|
||||||
PLEX_TO_KODI_TIMEFACTOR = 1.0 / 1000.0
|
PLEX_TO_KODI_TIMEFACTOR = 1.0 / 1000.0
|
||||||
|
|
||||||
# Possible output of Kodi's ListItem.DBTYPE for all video items
|
|
||||||
|
# All the Plex types as communicated in the PMS xml replies
|
||||||
|
PLEX_TYPE_VIDEO = 'video'
|
||||||
|
PLEX_TYPE_MOVIE = 'movie'
|
||||||
|
PLEX_TYPE_CLIP = 'clip' # e.g. trailers
|
||||||
|
|
||||||
|
PLEX_TYPE_EPISODE = 'episode'
|
||||||
|
PLEX_TYPE_SEASON = 'season'
|
||||||
|
PLEX_TYPE_SHOW = 'show'
|
||||||
|
|
||||||
|
PLEX_TYPE_AUDIO = 'music'
|
||||||
|
PLEX_TYPE_SONG = 'track'
|
||||||
|
PLEX_TYPE_ALBUM = 'album'
|
||||||
|
PLEX_TYPE_ARTIST = 'artist'
|
||||||
|
|
||||||
|
PLEX_TYPE_PHOTO = 'photo'
|
||||||
|
|
||||||
|
|
||||||
|
# All the Kodi types as e.g. used in the JSON API
|
||||||
|
KODI_TYPE_VIDEO = 'video'
|
||||||
|
KODI_TYPE_MOVIE = 'movie'
|
||||||
|
KODI_TYPE_SET = 'set' # for movie sets of several movies
|
||||||
|
KODI_TYPE_CLIP = 'clip' # e.g. trailers
|
||||||
|
|
||||||
|
KODI_TYPE_EPISODE = 'episode'
|
||||||
|
KODI_TYPE_SEASON = 'season'
|
||||||
|
KODI_TYPE_SHOW = 'tvshow'
|
||||||
|
|
||||||
|
KODI_TYPE_AUDIO = 'audio'
|
||||||
|
KODI_TYPE_SONG = 'song'
|
||||||
|
KODI_TYPE_ALBUM = 'album'
|
||||||
|
KODI_TYPE_ARTIST = 'artist'
|
||||||
|
|
||||||
|
KODI_TYPE_PHOTO = 'photo'
|
||||||
|
|
||||||
|
|
||||||
|
# Translation tables
|
||||||
|
|
||||||
KODI_VIDEOTYPES = (
|
KODI_VIDEOTYPES = (
|
||||||
'video',
|
KODI_TYPE_VIDEO,
|
||||||
'movie',
|
KODI_TYPE_MOVIE,
|
||||||
'set',
|
KODI_TYPE_SHOW,
|
||||||
'tvshow',
|
KODI_TYPE_SEASON,
|
||||||
'season',
|
KODI_TYPE_EPISODE,
|
||||||
'episode',
|
KODI_TYPE_SET
|
||||||
'musicvideo'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Possible output of Kodi's ListItem.DBTYPE for all audio items
|
|
||||||
KODI_AUDIOTYPES = (
|
KODI_AUDIOTYPES = (
|
||||||
'music',
|
KODI_TYPE_SONG,
|
||||||
'song',
|
KODI_TYPE_ALBUM,
|
||||||
'album',
|
KODI_TYPE_ARTIST,
|
||||||
'artist'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
ITEMTYPE_FROM_PLEXTYPE = {
|
ITEMTYPE_FROM_PLEXTYPE = {
|
||||||
'movie': 'Movies',
|
PLEX_TYPE_MOVIE: 'Movies',
|
||||||
'season': 'TVShows',
|
PLEX_TYPE_SEASON: 'TVShows',
|
||||||
'episode': 'TVShows',
|
KODI_TYPE_EPISODE: 'TVShows',
|
||||||
'show': 'TVShows',
|
PLEX_TYPE_SHOW: 'TVShows',
|
||||||
'artist': 'Music',
|
PLEX_TYPE_ARTIST: 'Music',
|
||||||
'album': 'Music',
|
PLEX_TYPE_ALBUM: 'Music',
|
||||||
'track': 'Music',
|
PLEX_TYPE_SONG: 'Music',
|
||||||
'song': 'Music'
|
}
|
||||||
|
|
||||||
|
ITEMTYPE_FROM_KODITYPE = {
|
||||||
|
KODI_TYPE_MOVIE: 'Movies',
|
||||||
|
KODI_TYPE_SEASON: 'TVShows',
|
||||||
|
KODI_TYPE_EPISODE: 'TVShows',
|
||||||
|
KODI_TYPE_SHOW: 'TVShows',
|
||||||
|
KODI_TYPE_ARTIST: 'Music',
|
||||||
|
KODI_TYPE_ALBUM: 'Music',
|
||||||
|
KODI_TYPE_SONG: 'Music',
|
||||||
}
|
}
|
||||||
|
|
||||||
KODITYPE_FROM_PLEXTYPE = {
|
KODITYPE_FROM_PLEXTYPE = {
|
||||||
'movie': 'movie',
|
PLEX_TYPE_MOVIE: KODI_TYPE_MOVIE,
|
||||||
'episode': 'episode',
|
PLEX_TYPE_EPISODE: KODI_TYPE_EPISODE,
|
||||||
'track': 'song',
|
PLEX_TYPE_SEASON: KODI_TYPE_SEASON,
|
||||||
'artist': 'artist',
|
PLEX_TYPE_SHOW: KODI_TYPE_SHOW,
|
||||||
'album': 'album',
|
PLEX_TYPE_SONG: KODI_TYPE_SONG,
|
||||||
|
PLEX_TYPE_ARTIST: KODI_TYPE_ARTIST,
|
||||||
|
PLEX_TYPE_ALBUM: KODI_TYPE_ALBUM,
|
||||||
|
PLEX_TYPE_PHOTO: KODI_TYPE_PHOTO,
|
||||||
'XXXXXX': 'musicvideo',
|
'XXXXXX': 'musicvideo',
|
||||||
'XXXXXXX': 'genre'
|
'XXXXXXX': 'genre'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE = {
|
||||||
|
PLEX_TYPE_VIDEO: KODI_TYPE_VIDEO,
|
||||||
|
PLEX_TYPE_MOVIE: KODI_TYPE_VIDEO,
|
||||||
|
PLEX_TYPE_EPISODE: KODI_TYPE_VIDEO,
|
||||||
|
PLEX_TYPE_SEASON: KODI_TYPE_VIDEO,
|
||||||
|
PLEX_TYPE_SHOW: KODI_TYPE_VIDEO,
|
||||||
|
PLEX_TYPE_CLIP: KODI_TYPE_VIDEO,
|
||||||
|
PLEX_TYPE_ARTIST: KODI_TYPE_AUDIO,
|
||||||
|
PLEX_TYPE_ALBUM: KODI_TYPE_AUDIO,
|
||||||
|
PLEX_TYPE_SONG: KODI_TYPE_AUDIO,
|
||||||
|
PLEX_TYPE_AUDIO: KODI_TYPE_AUDIO
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
REMAP_TYPE_FROM_PLEXTYPE = {
|
||||||
|
PLEX_TYPE_MOVIE: 'movie',
|
||||||
|
PLEX_TYPE_CLIP: 'clip',
|
||||||
|
PLEX_TYPE_SHOW: 'tv',
|
||||||
|
PLEX_TYPE_SEASON: 'tv',
|
||||||
|
PLEX_TYPE_EPISODE: 'tv',
|
||||||
|
PLEX_TYPE_ARTIST: 'music',
|
||||||
|
PLEX_TYPE_ALBUM: 'music',
|
||||||
|
PLEX_TYPE_SONG: 'music',
|
||||||
|
PLEX_TYPE_PHOTO: 'photo'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
REMAP_TYPE_FROM_PLEXTYPE = {
|
REMAP_TYPE_FROM_PLEXTYPE = {
|
||||||
'movie': 'movie',
|
'movie': 'movie',
|
||||||
|
@ -159,22 +231,6 @@ def SelectStreams(url, args):
|
||||||
url + '?' + urlencode(args), action_type='PUT')
|
url + '?' + urlencode(args), action_type='PUT')
|
||||||
|
|
||||||
|
|
||||||
def GetPlayQueue(playQueueID):
|
|
||||||
"""
|
|
||||||
Fetches the PMS playqueue with the playQueueID as an XML
|
|
||||||
|
|
||||||
Returns None if something went wrong
|
|
||||||
"""
|
|
||||||
url = "{server}/playQueues/%s" % playQueueID
|
|
||||||
args = {'Accept': 'application/xml'}
|
|
||||||
xml = downloadutils.DownloadUtils().downloadUrl(url, headerOptions=args)
|
|
||||||
try:
|
|
||||||
xml.attrib['playQueueID']
|
|
||||||
except (AttributeError, KeyError):
|
|
||||||
return None
|
|
||||||
return xml
|
|
||||||
|
|
||||||
|
|
||||||
def GetPlexMetadata(key):
|
def GetPlexMetadata(key):
|
||||||
"""
|
"""
|
||||||
Returns raw API metadata for key as an etree XML.
|
Returns raw API metadata for key as an etree XML.
|
||||||
|
@ -388,23 +444,22 @@ def GetPlexCollections(mediatype):
|
||||||
return collections
|
return collections
|
||||||
|
|
||||||
|
|
||||||
def GetPlexPlaylist(itemid, librarySectionUUID, mediatype='movie'):
|
def GetPlexPlaylist(itemid, librarySectionUUID, mediatype='movie',
|
||||||
|
trailers=False):
|
||||||
"""
|
"""
|
||||||
Returns raw API metadata XML dump for a playlist with e.g. trailers.
|
Returns raw API metadata XML dump for a playlist with e.g. trailers.
|
||||||
"""
|
"""
|
||||||
trailerNumber = settings('trailerNumber')
|
|
||||||
if not trailerNumber:
|
|
||||||
trailerNumber = '3'
|
|
||||||
url = "{server}/playQueues"
|
url = "{server}/playQueues"
|
||||||
args = {
|
args = {
|
||||||
'type': mediatype,
|
'type': mediatype,
|
||||||
'uri': ('library://' + librarySectionUUID +
|
'uri': ('library://' + librarySectionUUID +
|
||||||
'/item/%2Flibrary%2Fmetadata%2F' + itemid),
|
'/item/%2Flibrary%2Fmetadata%2F' + itemid),
|
||||||
'includeChapters': '1',
|
'includeChapters': '1',
|
||||||
'extrasPrefixCount': trailerNumber,
|
|
||||||
'shuffle': '0',
|
'shuffle': '0',
|
||||||
'repeat': '0'
|
'repeat': '0'
|
||||||
}
|
}
|
||||||
|
if trailers is True:
|
||||||
|
args['extrasPrefixCount'] = settings('trailerNumber')
|
||||||
xml = downloadutils.DownloadUtils().downloadUrl(
|
xml = downloadutils.DownloadUtils().downloadUrl(
|
||||||
url + '?' + urlencode(args), action_type="POST")
|
url + '?' + urlencode(args), action_type="POST")
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -281,7 +281,7 @@ class Artwork():
|
||||||
def cacheTexture(self, url):
|
def cacheTexture(self, url):
|
||||||
# Cache a single image url to the texture cache
|
# Cache a single image url to the texture cache
|
||||||
if url and self.enableTextureCache:
|
if url and self.enableTextureCache:
|
||||||
self.queue.put(double_urlencode(url))
|
self.queue.put(double_urlencode(tryEncode(url)))
|
||||||
|
|
||||||
def addArtwork(self, artwork, kodiId, mediaType, cursor):
|
def addArtwork(self, artwork, kodiId, mediaType, cursor):
|
||||||
# Kodi conversion table
|
# Kodi conversion table
|
||||||
|
|
|
@ -49,7 +49,7 @@ class ClientInfo():
|
||||||
'X-Plex-Product': self.getAddonName(),
|
'X-Plex-Product': self.getAddonName(),
|
||||||
'X-Plex-Version': self.getVersion(),
|
'X-Plex-Version': self.getVersion(),
|
||||||
'X-Plex-Client-Identifier': self.getDeviceId(),
|
'X-Plex-Client-Identifier': self.getDeviceId(),
|
||||||
'X-Plex-Provides': 'player',
|
'X-Plex-Provides': 'client,controller,player',
|
||||||
}
|
}
|
||||||
|
|
||||||
if window('pms_token'):
|
if window('pms_token'):
|
||||||
|
|
|
@ -8,7 +8,7 @@ import xbmc
|
||||||
import xbmcaddon
|
import xbmcaddon
|
||||||
|
|
||||||
import PlexFunctions as PF
|
import PlexFunctions as PF
|
||||||
import embydb_functions as embydb
|
import plexdb_functions as plexdb
|
||||||
from utils import window, settings, dialog, language as lang, kodiSQL
|
from utils import window, settings, dialog, language as lang, kodiSQL
|
||||||
from dialogs import context
|
from dialogs import context
|
||||||
|
|
||||||
|
@ -75,8 +75,8 @@ class ContextMenu(object):
|
||||||
def _get_item_id(cls, kodi_id, item_type):
|
def _get_item_id(cls, kodi_id, item_type):
|
||||||
item_id = xbmc.getInfoLabel('ListItem.Property(plexid)')
|
item_id = xbmc.getInfoLabel('ListItem.Property(plexid)')
|
||||||
if not item_id and kodi_id and item_type:
|
if not item_id and kodi_id and item_type:
|
||||||
with embydb.GetEmbyDB() as emby_db:
|
with plexdb.Get_Plex_DB() as plexcursor:
|
||||||
item = emby_db.getItem_byKodiId(kodi_id, item_type)
|
item = plexcursor.getItem_byKodiId(kodi_id, item_type)
|
||||||
try:
|
try:
|
||||||
item_id = item[0]
|
item_id = item[0]
|
||||||
except TypeError:
|
except TypeError:
|
||||||
|
@ -140,8 +140,8 @@ class ContextMenu(object):
|
||||||
elif selected == OPTIONS['PMS_Play']:
|
elif selected == OPTIONS['PMS_Play']:
|
||||||
self._PMS_play()
|
self._PMS_play()
|
||||||
|
|
||||||
elif selected == OPTIONS['Refresh']:
|
# elif selected == OPTIONS['Refresh']:
|
||||||
self.emby.refreshItem(self.item_id)
|
# self.emby.refreshItem(self.item_id)
|
||||||
|
|
||||||
# elif selected == OPTIONS['AddFav']:
|
# elif selected == OPTIONS['AddFav']:
|
||||||
# self.emby.updateUserRating(self.item_id, favourite=True)
|
# self.emby.updateUserRating(self.item_id, favourite=True)
|
||||||
|
@ -212,6 +212,6 @@ class ContextMenu(object):
|
||||||
'mode': "play"
|
'mode': "play"
|
||||||
}
|
}
|
||||||
from urllib import urlencode
|
from urllib import urlencode
|
||||||
handle = ("plugin://plugin.video.plexkodiconnect.movies?%s"
|
handle = ("plugin://plugin.video.plexkodiconnect/movies?%s"
|
||||||
% urlencode(params))
|
% urlencode(params))
|
||||||
xbmc.executebuiltin('RunPlugin(%s)' % handle)
|
xbmc.executebuiltin('RunPlugin(%s)' % handle)
|
||||||
|
|
|
@ -67,7 +67,7 @@ class UsersConnect(xbmcgui.WindowXMLDialog):
|
||||||
if self.kodi_version > 15:
|
if self.kodi_version > 15:
|
||||||
item.setArt({'Icon': user_image})
|
item.setArt({'Icon': user_image})
|
||||||
else:
|
else:
|
||||||
item.setIconImage(user_image)
|
item.setArt({'icon': user_image})
|
||||||
|
|
||||||
return item
|
return item
|
||||||
|
|
||||||
|
|
|
@ -1,402 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
|
|
||||||
from utils import kodiSQL
|
|
||||||
import logging
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
|
|
||||||
log = logging.getLogger("PLEX."+__name__)
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
|
|
||||||
|
|
||||||
class GetEmbyDB():
|
|
||||||
"""
|
|
||||||
Usage: with GetEmbyDB() as emby_db:
|
|
||||||
do stuff with emby_db
|
|
||||||
|
|
||||||
On exiting "with" (no matter what), commits get automatically committed
|
|
||||||
and the db gets closed
|
|
||||||
"""
|
|
||||||
def __enter__(self):
|
|
||||||
self.embyconn = kodiSQL('emby')
|
|
||||||
self.emby_db = Embydb_Functions(self.embyconn.cursor())
|
|
||||||
return self.emby_db
|
|
||||||
|
|
||||||
def __exit__(self, type, value, traceback):
|
|
||||||
self.embyconn.commit()
|
|
||||||
self.embyconn.close()
|
|
||||||
|
|
||||||
|
|
||||||
class Embydb_Functions():
|
|
||||||
|
|
||||||
def __init__(self, embycursor):
|
|
||||||
|
|
||||||
self.embycursor = embycursor
|
|
||||||
|
|
||||||
def getViews(self):
|
|
||||||
|
|
||||||
views = []
|
|
||||||
|
|
||||||
query = ' '.join((
|
|
||||||
|
|
||||||
"SELECT view_id",
|
|
||||||
"FROM view"
|
|
||||||
))
|
|
||||||
self.embycursor.execute(query)
|
|
||||||
rows = self.embycursor.fetchall()
|
|
||||||
for row in rows:
|
|
||||||
views.append(row[0])
|
|
||||||
return views
|
|
||||||
|
|
||||||
def getAllViewInfo(self):
|
|
||||||
|
|
||||||
embycursor = self.embycursor
|
|
||||||
views = []
|
|
||||||
|
|
||||||
query = ' '.join((
|
|
||||||
|
|
||||||
"SELECT view_id, view_name, media_type",
|
|
||||||
"FROM view"
|
|
||||||
))
|
|
||||||
embycursor.execute(query)
|
|
||||||
rows = embycursor.fetchall()
|
|
||||||
for row in rows:
|
|
||||||
views.append({'id': row[0],
|
|
||||||
'name': row[1],
|
|
||||||
'itemtype': row[2]})
|
|
||||||
return views
|
|
||||||
|
|
||||||
def getView_byId(self, viewid):
|
|
||||||
|
|
||||||
|
|
||||||
query = ' '.join((
|
|
||||||
|
|
||||||
"SELECT view_name, media_type, kodi_tagid",
|
|
||||||
"FROM view",
|
|
||||||
"WHERE view_id = ?"
|
|
||||||
))
|
|
||||||
self.embycursor.execute(query, (viewid,))
|
|
||||||
view = self.embycursor.fetchone()
|
|
||||||
|
|
||||||
return view
|
|
||||||
|
|
||||||
def getView_byType(self, mediatype):
|
|
||||||
|
|
||||||
views = []
|
|
||||||
|
|
||||||
query = ' '.join((
|
|
||||||
|
|
||||||
"SELECT view_id, view_name, media_type",
|
|
||||||
"FROM view",
|
|
||||||
"WHERE media_type = ?"
|
|
||||||
))
|
|
||||||
self.embycursor.execute(query, (mediatype,))
|
|
||||||
rows = self.embycursor.fetchall()
|
|
||||||
for row in rows:
|
|
||||||
views.append({
|
|
||||||
|
|
||||||
'id': row[0],
|
|
||||||
'name': row[1],
|
|
||||||
'itemtype': row[2]
|
|
||||||
})
|
|
||||||
|
|
||||||
return views
|
|
||||||
|
|
||||||
def getView_byName(self, tagname):
|
|
||||||
|
|
||||||
query = ' '.join((
|
|
||||||
|
|
||||||
"SELECT view_id",
|
|
||||||
"FROM view",
|
|
||||||
"WHERE view_name = ?"
|
|
||||||
))
|
|
||||||
self.embycursor.execute(query, (tagname,))
|
|
||||||
try:
|
|
||||||
view = self.embycursor.fetchone()[0]
|
|
||||||
|
|
||||||
except TypeError:
|
|
||||||
view = None
|
|
||||||
|
|
||||||
return view
|
|
||||||
|
|
||||||
def addView(self, plexid, name, mediatype, tagid):
|
|
||||||
|
|
||||||
query = (
|
|
||||||
'''
|
|
||||||
INSERT INTO view(
|
|
||||||
view_id, view_name, media_type, kodi_tagid)
|
|
||||||
|
|
||||||
VALUES (?, ?, ?, ?)
|
|
||||||
'''
|
|
||||||
)
|
|
||||||
self.embycursor.execute(query, (plexid, name, mediatype, tagid))
|
|
||||||
|
|
||||||
def updateView(self, name, tagid, mediafolderid):
|
|
||||||
|
|
||||||
query = ' '.join((
|
|
||||||
|
|
||||||
"UPDATE view",
|
|
||||||
"SET view_name = ?, kodi_tagid = ?",
|
|
||||||
"WHERE view_id = ?"
|
|
||||||
))
|
|
||||||
self.embycursor.execute(query, (name, tagid, mediafolderid))
|
|
||||||
|
|
||||||
def removeView(self, viewid):
|
|
||||||
|
|
||||||
query = ' '.join((
|
|
||||||
|
|
||||||
"DELETE FROM view",
|
|
||||||
"WHERE view_id = ?"
|
|
||||||
))
|
|
||||||
self.embycursor.execute(query, (viewid,))
|
|
||||||
|
|
||||||
def getItem_byFileId(self, fileId, kodiType):
|
|
||||||
"""
|
|
||||||
Returns the Plex itemId by using the Kodi fileId. VIDEO ONLY
|
|
||||||
|
|
||||||
kodiType: 'movie', 'episode', ...
|
|
||||||
"""
|
|
||||||
query = ' '.join((
|
|
||||||
"SELECT emby_id",
|
|
||||||
"FROM emby",
|
|
||||||
"WHERE kodi_fileid = ? AND media_type = ?"
|
|
||||||
))
|
|
||||||
try:
|
|
||||||
self.embycursor.execute(query, (fileId, kodiType))
|
|
||||||
item = self.embycursor.fetchone()[0]
|
|
||||||
return item
|
|
||||||
except:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def getMusicItem_byFileId(self, fileId, kodiType):
|
|
||||||
"""
|
|
||||||
Returns the Plex itemId by using the Kodi fileId. MUSIC ONLY
|
|
||||||
|
|
||||||
kodiType: 'song'
|
|
||||||
"""
|
|
||||||
query = ' '.join((
|
|
||||||
"SELECT emby_id",
|
|
||||||
"FROM emby",
|
|
||||||
"WHERE kodi_id = ? AND media_type = ?"
|
|
||||||
))
|
|
||||||
try:
|
|
||||||
self.embycursor.execute(query, (fileId, kodiType))
|
|
||||||
item = self.embycursor.fetchone()[0]
|
|
||||||
return item
|
|
||||||
except:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def getItem_byId(self, plexid):
|
|
||||||
|
|
||||||
query = ' '.join((
|
|
||||||
|
|
||||||
"SELECT kodi_id, kodi_fileid, kodi_pathid, parent_id, media_type, emby_type",
|
|
||||||
"FROM emby",
|
|
||||||
"WHERE emby_id = ?"
|
|
||||||
))
|
|
||||||
try:
|
|
||||||
self.embycursor.execute(query, (plexid,))
|
|
||||||
item = self.embycursor.fetchone()
|
|
||||||
return item
|
|
||||||
except: return None
|
|
||||||
|
|
||||||
def getItem_byWildId(self, plexid):
|
|
||||||
|
|
||||||
query = ' '.join((
|
|
||||||
|
|
||||||
"SELECT kodi_id, media_type",
|
|
||||||
"FROM emby",
|
|
||||||
"WHERE emby_id LIKE ?"
|
|
||||||
))
|
|
||||||
self.embycursor.execute(query, (plexid+"%",))
|
|
||||||
return self.embycursor.fetchall()
|
|
||||||
|
|
||||||
def getItem_byView(self, mediafolderid):
|
|
||||||
|
|
||||||
query = ' '.join((
|
|
||||||
|
|
||||||
"SELECT kodi_id",
|
|
||||||
"FROM emby",
|
|
||||||
"WHERE media_folder = ?"
|
|
||||||
))
|
|
||||||
self.embycursor.execute(query, (mediafolderid,))
|
|
||||||
return self.embycursor.fetchall()
|
|
||||||
|
|
||||||
def getPlexId(self, kodiid, mediatype):
|
|
||||||
"""
|
|
||||||
Returns the Plex ID usind the Kodiid. Result:
|
|
||||||
(Plex Id, Parent's Plex Id)
|
|
||||||
"""
|
|
||||||
query = ' '.join((
|
|
||||||
"SELECT emby_id, parent_id",
|
|
||||||
"FROM emby",
|
|
||||||
"WHERE kodi_id = ? AND media_type = ?"
|
|
||||||
))
|
|
||||||
try:
|
|
||||||
self.embycursor.execute(query, (kodiid, mediatype))
|
|
||||||
item = self.embycursor.fetchone()
|
|
||||||
return item
|
|
||||||
except:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def getItem_byKodiId(self, kodiid, mediatype):
|
|
||||||
|
|
||||||
query = ' '.join((
|
|
||||||
|
|
||||||
"SELECT emby_id, parent_id",
|
|
||||||
"FROM emby",
|
|
||||||
"WHERE kodi_id = ?",
|
|
||||||
"AND media_type = ?"
|
|
||||||
))
|
|
||||||
self.embycursor.execute(query, (kodiid, mediatype,))
|
|
||||||
return self.embycursor.fetchone()
|
|
||||||
|
|
||||||
def getItem_byParentId(self, parentid, mediatype):
|
|
||||||
|
|
||||||
query = ' '.join((
|
|
||||||
|
|
||||||
"SELECT emby_id, kodi_id, kodi_fileid",
|
|
||||||
"FROM emby",
|
|
||||||
"WHERE parent_id = ?",
|
|
||||||
"AND media_type = ?"
|
|
||||||
))
|
|
||||||
self.embycursor.execute(query, (parentid, mediatype,))
|
|
||||||
return self.embycursor.fetchall()
|
|
||||||
|
|
||||||
def getItemId_byParentId(self, parentid, mediatype):
|
|
||||||
|
|
||||||
query = ' '.join((
|
|
||||||
|
|
||||||
"SELECT emby_id, kodi_id",
|
|
||||||
"FROM emby",
|
|
||||||
"WHERE parent_id = ?",
|
|
||||||
"AND media_type = ?"
|
|
||||||
))
|
|
||||||
self.embycursor.execute(query, (parentid, mediatype,))
|
|
||||||
return self.embycursor.fetchall()
|
|
||||||
|
|
||||||
def getChecksum(self, mediatype):
|
|
||||||
|
|
||||||
query = ' '.join((
|
|
||||||
|
|
||||||
"SELECT emby_id, checksum",
|
|
||||||
"FROM emby",
|
|
||||||
"WHERE emby_type = ?"
|
|
||||||
))
|
|
||||||
self.embycursor.execute(query, (mediatype,))
|
|
||||||
return self.embycursor.fetchall()
|
|
||||||
|
|
||||||
def getMediaType_byId(self, plexid):
|
|
||||||
|
|
||||||
query = ' '.join((
|
|
||||||
|
|
||||||
"SELECT emby_type",
|
|
||||||
"FROM emby",
|
|
||||||
"WHERE emby_id = ?"
|
|
||||||
))
|
|
||||||
self.embycursor.execute(query, (plexid,))
|
|
||||||
try:
|
|
||||||
itemtype = self.embycursor.fetchone()[0]
|
|
||||||
|
|
||||||
except TypeError:
|
|
||||||
itemtype = None
|
|
||||||
|
|
||||||
return itemtype
|
|
||||||
|
|
||||||
def sortby_mediaType(self, itemids, unsorted=True):
|
|
||||||
|
|
||||||
sorted_items = {}
|
|
||||||
|
|
||||||
for itemid in itemids:
|
|
||||||
mediatype = self.getMediaType_byId(itemid)
|
|
||||||
if mediatype:
|
|
||||||
sorted_items.setdefault(mediatype, []).append(itemid)
|
|
||||||
elif unsorted:
|
|
||||||
sorted_items.setdefault('Unsorted', []).append(itemid)
|
|
||||||
|
|
||||||
return sorted_items
|
|
||||||
|
|
||||||
def addReference(self, plexid, kodiid, embytype, mediatype, fileid=None, pathid=None,
|
|
||||||
parentid=None, checksum=None, mediafolderid=None):
|
|
||||||
query = (
|
|
||||||
'''
|
|
||||||
INSERT OR REPLACE INTO emby(
|
|
||||||
emby_id, kodi_id, kodi_fileid, kodi_pathid, emby_type, media_type, parent_id,
|
|
||||||
checksum, media_folder)
|
|
||||||
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
||||||
'''
|
|
||||||
)
|
|
||||||
self.embycursor.execute(query, (plexid, kodiid, fileid, pathid, embytype, mediatype,
|
|
||||||
parentid, checksum, mediafolderid))
|
|
||||||
|
|
||||||
def updateReference(self, plexid, checksum):
|
|
||||||
|
|
||||||
query = "UPDATE emby SET checksum = ? WHERE emby_id = ?"
|
|
||||||
self.embycursor.execute(query, (checksum, plexid))
|
|
||||||
|
|
||||||
def updateParentId(self, plexid, parent_kodiid):
|
|
||||||
|
|
||||||
query = "UPDATE emby SET parent_id = ? WHERE emby_id = ?"
|
|
||||||
self.embycursor.execute(query, (parent_kodiid, plexid))
|
|
||||||
|
|
||||||
def removeItems_byParentId(self, parent_kodiid, mediatype):
|
|
||||||
|
|
||||||
query = ' '.join((
|
|
||||||
|
|
||||||
"DELETE FROM emby",
|
|
||||||
"WHERE parent_id = ?",
|
|
||||||
"AND media_type = ?"
|
|
||||||
))
|
|
||||||
self.embycursor.execute(query, (parent_kodiid, mediatype,))
|
|
||||||
|
|
||||||
def removeItem_byKodiId(self, kodiid, mediatype):
|
|
||||||
|
|
||||||
query = ' '.join((
|
|
||||||
|
|
||||||
"DELETE FROM emby",
|
|
||||||
"WHERE kodi_id = ?",
|
|
||||||
"AND media_type = ?"
|
|
||||||
))
|
|
||||||
self.embycursor.execute(query, (kodiid, mediatype,))
|
|
||||||
|
|
||||||
def removeItem(self, plexid):
|
|
||||||
|
|
||||||
query = "DELETE FROM emby WHERE emby_id = ?"
|
|
||||||
self.embycursor.execute(query, (plexid,))
|
|
||||||
|
|
||||||
def removeWildItem(self, plexid):
|
|
||||||
|
|
||||||
query = "DELETE FROM emby WHERE emby_id LIKE ?"
|
|
||||||
self.embycursor.execute(query, (plexid+"%",))
|
|
||||||
|
|
||||||
def itemsByType(self, plextype):
|
|
||||||
"""
|
|
||||||
Returns a list of dictionaries for all Kodi DB items present for
|
|
||||||
plextype. One dict is of the type
|
|
||||||
|
|
||||||
{
|
|
||||||
'plexId': the Plex id
|
|
||||||
'kodiId': the Kodi id
|
|
||||||
'kodi_type': e.g. 'movie', 'tvshow'
|
|
||||||
'plex_type': e.g. 'Movie', 'Series', the input plextype
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
query = ' '.join((
|
|
||||||
"SELECT emby_id, kodi_id, media_type",
|
|
||||||
"FROM emby",
|
|
||||||
"WHERE emby_type = ?",
|
|
||||||
))
|
|
||||||
self.embycursor.execute(query, (plextype, ))
|
|
||||||
result = []
|
|
||||||
for row in self.embycursor.fetchall():
|
|
||||||
result.append({
|
|
||||||
'plexId': row[0],
|
|
||||||
'kodiId': row[1],
|
|
||||||
'kodi_type': row[2],
|
|
||||||
'plex_type': plextype
|
|
||||||
})
|
|
||||||
return result
|
|
|
@ -17,12 +17,13 @@ from utils import window, settings, language as lang
|
||||||
from utils import tryDecode, tryEncode, CatchExceptions
|
from utils import tryDecode, tryEncode, CatchExceptions
|
||||||
import clientinfo
|
import clientinfo
|
||||||
import downloadutils
|
import downloadutils
|
||||||
import embydb_functions as embydb
|
import plexdb_functions as plexdb
|
||||||
import playbackutils as pbutils
|
import playbackutils as pbutils
|
||||||
import playlist
|
|
||||||
|
|
||||||
import PlexFunctions
|
import PlexFunctions
|
||||||
import PlexAPI
|
import PlexAPI
|
||||||
|
from PKC_listitem import convert_PKC_to_listitem
|
||||||
|
from playqueue import Playqueue
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
@ -33,40 +34,6 @@ addonName = "PlexKodiConnect"
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
|
||||||
def plexCompanion(fullurl, params):
|
|
||||||
params = PlexFunctions.LiteralEval(params[26:])
|
|
||||||
|
|
||||||
if params['machineIdentifier'] != window('plex_machineIdentifier'):
|
|
||||||
log.error("Command was not for us, machineIdentifier controller: %s, "
|
|
||||||
"our machineIdentifier : %s"
|
|
||||||
% (params['machineIdentifier'],
|
|
||||||
window('plex_machineIdentifier')))
|
|
||||||
return
|
|
||||||
|
|
||||||
library, key, query = PlexFunctions.ParseContainerKey(
|
|
||||||
params['containerKey'])
|
|
||||||
# Construct a container key that works always (get rid of playlist args)
|
|
||||||
window('containerKey', '/'+library+'/'+key)
|
|
||||||
|
|
||||||
if 'playQueues' in library:
|
|
||||||
log.debug("Playing a playQueue. Query was: %s" % query)
|
|
||||||
# Playing a playlist that we need to fetch from PMS
|
|
||||||
xml = PlexFunctions.GetPlayQueue(key)
|
|
||||||
if xml is None:
|
|
||||||
log.error("Error getting PMS playlist for key %s" % key)
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
resume = PlexFunctions.ConvertPlexToKodiTime(
|
|
||||||
params.get('offset', 0))
|
|
||||||
itemids = []
|
|
||||||
for item in xml:
|
|
||||||
itemids.append(item.get('ratingKey'))
|
|
||||||
return playlist.Playlist().playAll(itemids, resume)
|
|
||||||
|
|
||||||
else:
|
|
||||||
log.error("Not knowing what to do for now - no playQueue sent")
|
|
||||||
|
|
||||||
|
|
||||||
def chooseServer():
|
def chooseServer():
|
||||||
"""
|
"""
|
||||||
Lets user choose from list of PMS
|
Lets user choose from list of PMS
|
||||||
|
@ -130,45 +97,21 @@ def togglePlexTV():
|
||||||
sound=False)
|
sound=False)
|
||||||
|
|
||||||
|
|
||||||
def PassPlaylist(xml, resume=None):
|
def Plex_Node(url, viewOffset, plex_type, playdirectly=False):
|
||||||
"""
|
|
||||||
resume in KodiTime - seconds.
|
|
||||||
"""
|
|
||||||
# Set window properties to make them available later for other threads
|
|
||||||
windowArgs = [
|
|
||||||
# 'containerKey'
|
|
||||||
'playQueueID',
|
|
||||||
'playQueueVersion']
|
|
||||||
for arg in windowArgs:
|
|
||||||
window(arg, value=xml.attrib.get(arg))
|
|
||||||
|
|
||||||
# Get resume point
|
|
||||||
from utils import IntFromStr
|
|
||||||
resume1 = PlexFunctions.ConvertPlexToKodiTime(IntFromStr(
|
|
||||||
xml.attrib.get('playQueueSelectedItemOffset', 0)))
|
|
||||||
resume2 = resume
|
|
||||||
resume = max(resume1, resume2)
|
|
||||||
|
|
||||||
pbutils.PlaybackUtils(xml).StartPlay(
|
|
||||||
resume=resume,
|
|
||||||
resumeId=xml.attrib.get('playQueueSelectedItemID', None))
|
|
||||||
|
|
||||||
|
|
||||||
def playWatchLater(itemid, viewOffset):
|
|
||||||
"""
|
"""
|
||||||
Called only for a SINGLE element for Plex.tv watch later
|
Called only for a SINGLE element for Plex.tv watch later
|
||||||
|
|
||||||
Always to return with a "setResolvedUrl"
|
Always to return with a "setResolvedUrl"
|
||||||
"""
|
"""
|
||||||
log.info('playWatchLater called with id: %s, viewOffset: %s'
|
log.info('Plex_Node called with url: %s, viewOffset: %s'
|
||||||
% (itemid, viewOffset))
|
% (url, viewOffset))
|
||||||
# Plex redirect, e.g. watch later. Need to get actual URLs
|
# Plex redirect, e.g. watch later. Need to get actual URLs
|
||||||
xml = downloadutils.DownloadUtils().downloadUrl(itemid,
|
xml = downloadutils.DownloadUtils().downloadUrl(url)
|
||||||
authenticate=False)
|
try:
|
||||||
if xml in (None, 401):
|
xml[0].attrib
|
||||||
log.error("Could not resolve url %s" % itemid)
|
except:
|
||||||
return xbmcplugin.setResolvedUrl(
|
log.error('Could not download PMS metadata')
|
||||||
int(sys.argv[1]), False, xbmcgui.ListItem())
|
return
|
||||||
if viewOffset != '0':
|
if viewOffset != '0':
|
||||||
try:
|
try:
|
||||||
viewOffset = int(PlexFunctions.PLEX_TO_KODI_TIMEFACTOR *
|
viewOffset = int(PlexFunctions.PLEX_TO_KODI_TIMEFACTOR *
|
||||||
|
@ -178,41 +121,20 @@ def playWatchLater(itemid, viewOffset):
|
||||||
else:
|
else:
|
||||||
window('plex_customplaylist.seektime', value=str(viewOffset))
|
window('plex_customplaylist.seektime', value=str(viewOffset))
|
||||||
log.info('Set resume point to %s' % str(viewOffset))
|
log.info('Set resume point to %s' % str(viewOffset))
|
||||||
return pbutils.PlaybackUtils(xml).play(None, 'plexnode')
|
typus = PlexFunctions.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[plex_type]
|
||||||
|
playqueue = Playqueue().get_playqueue_from_type(typus)
|
||||||
|
result = pbutils.PlaybackUtils(xml, playqueue).play(
|
||||||
def doPlayback(itemid, dbid):
|
None,
|
||||||
"""
|
kodi_id='plexnode',
|
||||||
Called only for a SINGLE element, not playQueues
|
plex_lib_UUID=xml.attrib.get('librarySectionUUID'))
|
||||||
|
if result.listitem:
|
||||||
Always to return with a "setResolvedUrl"
|
listitem = convert_PKC_to_listitem(result.listitem)
|
||||||
"""
|
|
||||||
if window('plex_authenticated') != "true":
|
|
||||||
log.error('Not yet authenticated for a PMS, abort starting playback')
|
|
||||||
# Not yet connected to a PMS server
|
|
||||||
xbmcgui.Dialog().notification(
|
|
||||||
addonName,
|
|
||||||
lang(39210),
|
|
||||||
xbmcgui.NOTIFICATION_ERROR,
|
|
||||||
7000,
|
|
||||||
True)
|
|
||||||
return xbmcplugin.setResolvedUrl(
|
|
||||||
int(sys.argv[1]), False, xbmcgui.ListItem())
|
|
||||||
|
|
||||||
xml = PlexFunctions.GetPlexMetadata(itemid)
|
|
||||||
if xml in (None, 401):
|
|
||||||
return xbmcplugin.setResolvedUrl(
|
|
||||||
int(sys.argv[1]), False, xbmcgui.ListItem())
|
|
||||||
if xml[0].attrib.get('type') == 'photo':
|
|
||||||
# Photo
|
|
||||||
API = PlexAPI.API(xml[0])
|
|
||||||
listitem = API.CreateListItemFromPlexItem()
|
|
||||||
API.AddStreamInfo(listitem)
|
|
||||||
pbutils.PlaybackUtils(xml[0]).setArtwork(listitem)
|
|
||||||
return xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem)
|
|
||||||
else:
|
else:
|
||||||
# Video
|
return
|
||||||
return pbutils.PlaybackUtils(xml).play(itemid, dbid)
|
if playdirectly:
|
||||||
|
xbmc.Player().play(listitem.getfilename(), listitem)
|
||||||
|
else:
|
||||||
|
xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem)
|
||||||
|
|
||||||
|
|
||||||
##### DO RESET AUTH #####
|
##### DO RESET AUTH #####
|
||||||
|
@ -319,12 +241,8 @@ def deleteItem():
|
||||||
log.error("Unknown type, unable to proceed.")
|
log.error("Unknown type, unable to proceed.")
|
||||||
return
|
return
|
||||||
|
|
||||||
from utils import kodiSQL
|
with plexdb.Get_Plex_DB() as plexcursor:
|
||||||
embyconn = kodiSQL('emby')
|
item = plexcursor.getItem_byKodiId(dbid, itemtype)
|
||||||
embycursor = embyconn.cursor()
|
|
||||||
emby_db = embydb.Embydb_Functions(embycursor)
|
|
||||||
item = emby_db.getItem_byKodiId(dbid, itemtype)
|
|
||||||
embycursor.close()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
plexid = item[0]
|
plexid = item[0]
|
||||||
|
@ -467,99 +385,6 @@ def BrowseContent(viewname, browse_type="", folderid=""):
|
||||||
|
|
||||||
xbmcplugin.endOfDirectory(handle=int(sys.argv[1]))
|
xbmcplugin.endOfDirectory(handle=int(sys.argv[1]))
|
||||||
|
|
||||||
##### CREATE LISTITEM FROM EMBY METADATA #####
|
|
||||||
# def createListItemFromEmbyItem(item,art=artwork.Artwork(),doUtils=downloadutils.DownloadUtils()):
|
|
||||||
def createListItemFromEmbyItem(item,art=None,doUtils=downloadutils.DownloadUtils()):
|
|
||||||
API = PlexAPI.API(item)
|
|
||||||
itemid = item['Id']
|
|
||||||
|
|
||||||
title = item.get('Name')
|
|
||||||
li = xbmcgui.ListItem(title)
|
|
||||||
|
|
||||||
premieredate = item.get('PremiereDate',"")
|
|
||||||
if not premieredate: premieredate = item.get('DateCreated',"")
|
|
||||||
if premieredate:
|
|
||||||
premieredatelst = premieredate.split('T')[0].split("-")
|
|
||||||
premieredate = "%s.%s.%s" %(premieredatelst[2],premieredatelst[1],premieredatelst[0])
|
|
||||||
|
|
||||||
li.setProperty("plexid",itemid)
|
|
||||||
|
|
||||||
allart = art.getAllArtwork(item)
|
|
||||||
|
|
||||||
if item["Type"] == "Photo":
|
|
||||||
#listitem setup for pictures...
|
|
||||||
img_path = allart.get('Primary')
|
|
||||||
li.setProperty("path",img_path)
|
|
||||||
picture = doUtils.downloadUrl("{server}/Items/%s/Images" %itemid)
|
|
||||||
if picture:
|
|
||||||
picture = picture[0]
|
|
||||||
if picture.get("Width") > picture.get("Height"):
|
|
||||||
li.setArt( {"fanart": img_path}) #add image as fanart for use with skinhelper auto thumb/backgrund creation
|
|
||||||
li.setInfo('pictures', infoLabels={ "picturepath": img_path, "date": premieredate, "size": picture.get("Size"), "exif:width": str(picture.get("Width")), "exif:height": str(picture.get("Height")), "title": title})
|
|
||||||
li.setThumbnailImage(img_path)
|
|
||||||
li.setProperty("plot",API.getOverview())
|
|
||||||
li.setIconImage('DefaultPicture.png')
|
|
||||||
else:
|
|
||||||
#normal video items
|
|
||||||
li.setProperty('IsPlayable', 'true')
|
|
||||||
path = "%s?id=%s&mode=play" % (sys.argv[0], item.get("Id"))
|
|
||||||
li.setProperty("path",path)
|
|
||||||
genre = API.getGenres()
|
|
||||||
overlay = 0
|
|
||||||
userdata = API.getUserData()
|
|
||||||
runtime = item.get("RunTimeTicks",0)/ 10000000.0
|
|
||||||
seektime = userdata['Resume']
|
|
||||||
if seektime:
|
|
||||||
li.setProperty("resumetime", str(seektime))
|
|
||||||
li.setProperty("totaltime", str(runtime))
|
|
||||||
|
|
||||||
played = userdata['Played']
|
|
||||||
if played: overlay = 7
|
|
||||||
else: overlay = 6
|
|
||||||
playcount = userdata['PlayCount']
|
|
||||||
if playcount is None:
|
|
||||||
playcount = 0
|
|
||||||
|
|
||||||
rating = item.get('CommunityRating')
|
|
||||||
if not rating: rating = userdata['UserRating']
|
|
||||||
|
|
||||||
# Populate the extradata list and artwork
|
|
||||||
extradata = {
|
|
||||||
'id': itemid,
|
|
||||||
'rating': rating,
|
|
||||||
'year': item.get('ProductionYear'),
|
|
||||||
'genre': genre,
|
|
||||||
'playcount': str(playcount),
|
|
||||||
'title': title,
|
|
||||||
'plot': API.getOverview(),
|
|
||||||
'Overlay': str(overlay),
|
|
||||||
'duration': runtime
|
|
||||||
}
|
|
||||||
if premieredate:
|
|
||||||
extradata["premieredate"] = premieredate
|
|
||||||
extradata["date"] = premieredate
|
|
||||||
li.setInfo('video', infoLabels=extradata)
|
|
||||||
if allart.get('Primary'):
|
|
||||||
li.setThumbnailImage(allart.get('Primary'))
|
|
||||||
else: li.setThumbnailImage('DefaultTVShows.png')
|
|
||||||
li.setIconImage('DefaultTVShows.png')
|
|
||||||
if not allart.get('Background'): #add image as fanart for use with skinhelper auto thumb/backgrund creation
|
|
||||||
li.setArt( {"fanart": allart.get('Primary') } )
|
|
||||||
else:
|
|
||||||
pbutils.PlaybackUtils(item).setArtwork(li)
|
|
||||||
|
|
||||||
mediastreams = API.getMediaStreams()
|
|
||||||
videostreamFound = False
|
|
||||||
if mediastreams:
|
|
||||||
for key, value in mediastreams.iteritems():
|
|
||||||
if key == "video" and value: videostreamFound = True
|
|
||||||
if value: li.addStreamInfo(key, value[0])
|
|
||||||
if not videostreamFound:
|
|
||||||
#just set empty streamdetails to prevent errors in the logs
|
|
||||||
li.addStreamInfo("video", {'duration': runtime})
|
|
||||||
|
|
||||||
return li
|
|
||||||
|
|
||||||
##### BROWSE EMBY CHANNELS #####
|
##### BROWSE EMBY CHANNELS #####
|
||||||
def BrowseChannels(itemid, folderid=None):
|
def BrowseChannels(itemid, folderid=None):
|
||||||
|
|
||||||
|
@ -664,7 +489,7 @@ def createListItem(item, appendShowTitle=False, appendSxxExx=False):
|
||||||
li.setProperty('totaltime', str(item['resume']['total']))
|
li.setProperty('totaltime', str(item['resume']['total']))
|
||||||
li.setArt(item['art'])
|
li.setArt(item['art'])
|
||||||
li.setThumbnailImage(item['art'].get('thumb',''))
|
li.setThumbnailImage(item['art'].get('thumb',''))
|
||||||
li.setIconImage('DefaultTVShows.png')
|
li.setArt({'icon': 'DefaultTVShows.png'})
|
||||||
li.setProperty('dbid', str(item['episodeid']))
|
li.setProperty('dbid', str(item['episodeid']))
|
||||||
li.setProperty('fanart_image', item['art'].get('tvshow.fanart',''))
|
li.setProperty('fanart_image', item['art'].get('tvshow.fanart',''))
|
||||||
for key, value in item['streamdetails'].iteritems():
|
for key, value in item['streamdetails'].iteritems():
|
||||||
|
@ -1095,14 +920,14 @@ def BrowsePlexContent(viewid, mediatype="", folderid=""):
|
||||||
li.setProperty('IsPlayable', 'false')
|
li.setProperty('IsPlayable', 'false')
|
||||||
path = "%s?id=%s&mode=browseplex&type=%s&folderid=%s" \
|
path = "%s?id=%s&mode=browseplex&type=%s&folderid=%s" \
|
||||||
% (sys.argv[0], viewid, mediatype, API.getKey())
|
% (sys.argv[0], viewid, mediatype, API.getKey())
|
||||||
pbutils.PlaybackUtils(item).setArtwork(li)
|
API.set_listitem_artwork(li)
|
||||||
xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]),
|
xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]),
|
||||||
url=path,
|
url=path,
|
||||||
listitem=li,
|
listitem=li,
|
||||||
isFolder=True)
|
isFolder=True)
|
||||||
else:
|
else:
|
||||||
li = API.CreateListItemFromPlexItem()
|
li = API.CreateListItemFromPlexItem()
|
||||||
pbutils.PlaybackUtils(item).setArtwork(li)
|
API.set_listitem_artwork(li)
|
||||||
xbmcplugin.addDirectoryItem(
|
xbmcplugin.addDirectoryItem(
|
||||||
handle=int(sys.argv[1]),
|
handle=int(sys.argv[1]),
|
||||||
url=li.getProperty("path"),
|
url=li.getProperty("path"),
|
||||||
|
@ -1159,7 +984,7 @@ def getOnDeck(viewid, mediatype, tagname, limit):
|
||||||
appendShowTitle=appendShowTitle,
|
appendShowTitle=appendShowTitle,
|
||||||
appendSxxExx=appendSxxExx)
|
appendSxxExx=appendSxxExx)
|
||||||
API.AddStreamInfo(listitem)
|
API.AddStreamInfo(listitem)
|
||||||
pbutils.PlaybackUtils(item).setArtwork(listitem)
|
API.set_listitem_artwork(listitem)
|
||||||
if directpaths:
|
if directpaths:
|
||||||
url = API.getFilePath()
|
url = API.getFilePath()
|
||||||
else:
|
else:
|
||||||
|
@ -1168,7 +993,7 @@ def getOnDeck(viewid, mediatype, tagname, limit):
|
||||||
'id': API.getRatingKey(),
|
'id': API.getRatingKey(),
|
||||||
'dbid': listitem.getProperty('dbid')
|
'dbid': listitem.getProperty('dbid')
|
||||||
}
|
}
|
||||||
url = "plugin://plugin.video.plexkodiconnect.tvshows/?%s" \
|
url = "plugin://plugin.video.plexkodiconnect/tvshows/?%s" \
|
||||||
% urllib.urlencode(params)
|
% urllib.urlencode(params)
|
||||||
xbmcplugin.addDirectoryItem(
|
xbmcplugin.addDirectoryItem(
|
||||||
handle=int(sys.argv[1]),
|
handle=int(sys.argv[1]),
|
||||||
|
@ -1306,15 +1131,16 @@ def watchlater():
|
||||||
xbmcplugin.setContent(int(sys.argv[1]), 'movies')
|
xbmcplugin.setContent(int(sys.argv[1]), 'movies')
|
||||||
url = "plugin://plugin.video.plexkodiconnect/"
|
url = "plugin://plugin.video.plexkodiconnect/"
|
||||||
params = {
|
params = {
|
||||||
'mode': "playwatchlater",
|
'mode': "Plex_Node",
|
||||||
}
|
}
|
||||||
for item in xml:
|
for item in xml:
|
||||||
API = PlexAPI.API(item)
|
API = PlexAPI.API(item)
|
||||||
listitem = API.CreateListItemFromPlexItem()
|
listitem = API.CreateListItemFromPlexItem()
|
||||||
API.AddStreamInfo(listitem)
|
API.AddStreamInfo(listitem)
|
||||||
pbutils.PlaybackUtils(item).setArtwork(listitem)
|
API.set_listitem_artwork(listitem)
|
||||||
params['id'] = item.attrib.get('key')
|
params['id'] = item.attrib.get('key')
|
||||||
params['viewOffset'] = item.attrib.get('viewOffset', '0')
|
params['viewOffset'] = item.attrib.get('viewOffset', '0')
|
||||||
|
params['plex_type'] = item.attrib.get('type')
|
||||||
xbmcplugin.addDirectoryItem(
|
xbmcplugin.addDirectoryItem(
|
||||||
handle=int(sys.argv[1]),
|
handle=int(sys.argv[1]),
|
||||||
url="%s?%s" % (url, urllib.urlencode(params)),
|
url="%s?%s" % (url, urllib.urlencode(params)),
|
||||||
|
|
|
@ -9,7 +9,7 @@ import xbmcgui
|
||||||
from utils import settings, window, language as lang
|
from utils import settings, window, language as lang
|
||||||
import clientinfo
|
import clientinfo
|
||||||
import downloadutils
|
import downloadutils
|
||||||
import userclient
|
from userclient import UserClient
|
||||||
|
|
||||||
import PlexAPI
|
import PlexAPI
|
||||||
from PlexFunctions import GetMachineIdentifier, get_PMS_settings
|
from PlexFunctions import GetMachineIdentifier, get_PMS_settings
|
||||||
|
@ -30,11 +30,10 @@ class InitialSetup():
|
||||||
self.clientInfo = clientinfo.ClientInfo()
|
self.clientInfo = clientinfo.ClientInfo()
|
||||||
self.addonId = self.clientInfo.getAddonId()
|
self.addonId = self.clientInfo.getAddonId()
|
||||||
self.doUtils = downloadutils.DownloadUtils().downloadUrl
|
self.doUtils = downloadutils.DownloadUtils().downloadUrl
|
||||||
self.userClient = userclient.UserClient()
|
|
||||||
self.plx = PlexAPI.PlexAPI()
|
self.plx = PlexAPI.PlexAPI()
|
||||||
self.dialog = xbmcgui.Dialog()
|
self.dialog = xbmcgui.Dialog()
|
||||||
|
|
||||||
self.server = self.userClient.getServer()
|
self.server = UserClient().getServer()
|
||||||
self.serverid = settings('plex_machineIdentifier')
|
self.serverid = settings('plex_machineIdentifier')
|
||||||
# Get Plex credentials from settings file, if they exist
|
# Get Plex credentials from settings file, if they exist
|
||||||
plexdict = self.plx.GetPlexLoginFromSettings()
|
plexdict = self.plx.GetPlexLoginFromSettings()
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -7,6 +7,7 @@ from ntpath import dirname
|
||||||
|
|
||||||
import artwork
|
import artwork
|
||||||
from utils import kodiSQL, KODIVERSION
|
from utils import kodiSQL, KODIVERSION
|
||||||
|
from PlexFunctions import KODI_TYPE_MOVIE, KODI_TYPE_EPISODE
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
@ -31,8 +32,8 @@ class GetKodiDB():
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
self.kodiconn = kodiSQL(self.itemType)
|
self.kodiconn = kodiSQL(self.itemType)
|
||||||
self.emby_db = Kodidb_Functions(self.kodiconn.cursor())
|
kodi_db = Kodidb_Functions(self.kodiconn.cursor())
|
||||||
return self.emby_db
|
return kodi_db
|
||||||
|
|
||||||
def __exit__(self, type, value, traceback):
|
def __exit__(self, type, value, traceback):
|
||||||
self.kodiconn.commit()
|
self.kodiconn.commit()
|
||||||
|
@ -61,7 +62,7 @@ class Kodidb_Functions():
|
||||||
self.cursor.execute(
|
self.cursor.execute(
|
||||||
query, ('movies',
|
query, ('movies',
|
||||||
'metadata.local',
|
'metadata.local',
|
||||||
'plugin://plugin.video.plexkodiconnect.movies%%'))
|
'plugin://plugin.video.plexkodiconnect/movies%%'))
|
||||||
|
|
||||||
def getParentPathId(self, path):
|
def getParentPathId(self, path):
|
||||||
"""
|
"""
|
||||||
|
@ -869,7 +870,7 @@ class Kodidb_Functions():
|
||||||
self.cursor.execute(query, (idFile,))
|
self.cursor.execute(query, (idFile,))
|
||||||
try:
|
try:
|
||||||
itemId = self.cursor.fetchone()[0]
|
itemId = self.cursor.fetchone()[0]
|
||||||
typus = 'movie'
|
typus = KODI_TYPE_MOVIE
|
||||||
except TypeError:
|
except TypeError:
|
||||||
# Try tv shows next
|
# Try tv shows next
|
||||||
query = ' '.join((
|
query = ' '.join((
|
||||||
|
@ -880,7 +881,7 @@ class Kodidb_Functions():
|
||||||
self.cursor.execute(query, (idFile,))
|
self.cursor.execute(query, (idFile,))
|
||||||
try:
|
try:
|
||||||
itemId = self.cursor.fetchone()[0]
|
itemId = self.cursor.fetchone()[0]
|
||||||
typus = 'episode'
|
typus = KODI_TYPE_EPISODE
|
||||||
except TypeError:
|
except TypeError:
|
||||||
log.warn('Unexpectantly did not find a match!')
|
log.warn('Unexpectantly did not find a match!')
|
||||||
return
|
return
|
||||||
|
@ -907,13 +908,13 @@ class Kodidb_Functions():
|
||||||
return ids
|
return ids
|
||||||
|
|
||||||
def getVideoRuntime(self, kodiid, mediatype):
|
def getVideoRuntime(self, kodiid, mediatype):
|
||||||
if mediatype == 'movie':
|
if mediatype == KODI_TYPE_MOVIE:
|
||||||
query = ' '.join((
|
query = ' '.join((
|
||||||
"SELECT c11",
|
"SELECT c11",
|
||||||
"FROM movie",
|
"FROM movie",
|
||||||
"WHERE idMovie = ?",
|
"WHERE idMovie = ?",
|
||||||
))
|
))
|
||||||
elif mediatype == 'episode':
|
elif mediatype == KODI_TYPE_EPISODE:
|
||||||
query = ' '.join((
|
query = ' '.join((
|
||||||
"SELECT c09",
|
"SELECT c09",
|
||||||
"FROM episode",
|
"FROM episode",
|
||||||
|
@ -1397,3 +1398,89 @@ class Kodidb_Functions():
|
||||||
|
|
||||||
query = "INSERT OR REPLACE INTO song_genre(idGenre, idSong) values(?, ?)"
|
query = "INSERT OR REPLACE INTO song_genre(idGenre, idSong) values(?, ?)"
|
||||||
self.cursor.execute(query, (genreid, kodiid))
|
self.cursor.execute(query, (genreid, kodiid))
|
||||||
|
|
||||||
|
# Krypton only stuff ##############################
|
||||||
|
|
||||||
|
def create_entry_uniqueid(self):
|
||||||
|
self.cursor.execute(
|
||||||
|
"select coalesce(max(uniqueid_id),0) from uniqueid")
|
||||||
|
return self.cursor.fetchone()[0] + 1
|
||||||
|
|
||||||
|
def add_uniqueid(self, *args):
|
||||||
|
"""
|
||||||
|
Feed with:
|
||||||
|
uniqueid_id, media_id, media_type, value, type
|
||||||
|
|
||||||
|
type: e.g. 'imdb'
|
||||||
|
"""
|
||||||
|
query = '''
|
||||||
|
INSERT INTO uniqueid(
|
||||||
|
uniqueid_id, media_id, media_type, value, type)
|
||||||
|
VALUES (?, ?, ?, ?, ?)
|
||||||
|
'''
|
||||||
|
self.cursor.execute(query, (args))
|
||||||
|
|
||||||
|
def create_entry_rating(self):
|
||||||
|
self.cursor.execute("select coalesce(max(rating_id),0) from rating")
|
||||||
|
return self.cursor.fetchone()[0] + 1
|
||||||
|
|
||||||
|
def get_ratingid(self, media_id):
|
||||||
|
query = "SELECT rating_id FROM rating WHERE media_id = ?"
|
||||||
|
self.cursor.execute(query, (media_id,))
|
||||||
|
try:
|
||||||
|
ratingid = self.cursor.fetchone()[0]
|
||||||
|
except TypeError:
|
||||||
|
ratingid = None
|
||||||
|
return ratingid
|
||||||
|
|
||||||
|
def update_ratings(self, *args):
|
||||||
|
"""
|
||||||
|
Feed with media_id, media_type, rating_type, rating, votes, rating_id
|
||||||
|
"""
|
||||||
|
query = '''
|
||||||
|
UPDATE rating
|
||||||
|
SET media_id = ?,
|
||||||
|
media_type = ?,
|
||||||
|
rating_type = ?,
|
||||||
|
rating = ?,
|
||||||
|
votes = ?
|
||||||
|
WHERE rating_id = ?
|
||||||
|
'''
|
||||||
|
self.cursor.execute(query, (args))
|
||||||
|
|
||||||
|
def add_ratings(self, *args):
|
||||||
|
"""
|
||||||
|
feed with:
|
||||||
|
rating_id, media_id, media_type, rating_type, rating, votes
|
||||||
|
|
||||||
|
rating_type = 'default'
|
||||||
|
"""
|
||||||
|
query = '''
|
||||||
|
INSERT INTO rating(
|
||||||
|
rating_id, media_id, media_type, rating_type, rating, votes)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
'''
|
||||||
|
self.cursor.execute(query, (args))
|
||||||
|
|
||||||
|
|
||||||
|
def get_kodiid_from_filename(file):
|
||||||
|
"""
|
||||||
|
Returns the tuple (kodiid, type) if we have a video in the database with
|
||||||
|
said filename, or (None, None)
|
||||||
|
"""
|
||||||
|
kodiid = None
|
||||||
|
typus = None
|
||||||
|
try:
|
||||||
|
filename = file.rsplit('/', 1)[1]
|
||||||
|
path = file.rsplit('/', 1)[0] + '/'
|
||||||
|
except IndexError:
|
||||||
|
filename = file.rsplit('\\', 1)[1]
|
||||||
|
path = file.rsplit('\\', 1)[0] + '\\'
|
||||||
|
log.debug('Trying to figure out playing item from filename: %s '
|
||||||
|
'and path: %s' % (filename, path))
|
||||||
|
with GetKodiDB('video') as kodi_db:
|
||||||
|
try:
|
||||||
|
kodiid, typus = kodi_db.getIdFromFilename(filename, path)
|
||||||
|
except TypeError:
|
||||||
|
log.info('No kodi video element found with filename %s' % filename)
|
||||||
|
return (kodiid, typus)
|
||||||
|
|
|
@ -3,17 +3,16 @@
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import json
|
from json import loads
|
||||||
|
|
||||||
import xbmc
|
from xbmc import Monitor, Player, sleep
|
||||||
import xbmcgui
|
|
||||||
|
|
||||||
import downloadutils
|
import downloadutils
|
||||||
import embydb_functions as embydb
|
import plexdb_functions as plexdb
|
||||||
import kodidb_functions as kodidb
|
|
||||||
import playbackutils as pbutils
|
|
||||||
from utils import window, settings, CatchExceptions, tryDecode, tryEncode
|
from utils import window, settings, CatchExceptions, tryDecode, tryEncode
|
||||||
from PlexFunctions import scrobble, REMAP_TYPE_FROM_PLEXTYPE
|
from PlexFunctions import scrobble, REMAP_TYPE_FROM_PLEXTYPE
|
||||||
|
from kodidb_functions import get_kodiid_from_filename
|
||||||
|
from PlexAPI import API
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
@ -22,13 +21,14 @@ log = logging.getLogger("PLEX."+__name__)
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
|
||||||
class KodiMonitor(xbmc.Monitor):
|
class KodiMonitor(Monitor):
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
|
|
||||||
|
def __init__(self, callback):
|
||||||
|
self.mgr = callback
|
||||||
self.doUtils = downloadutils.DownloadUtils().downloadUrl
|
self.doUtils = downloadutils.DownloadUtils().downloadUrl
|
||||||
self.xbmcplayer = xbmc.Player()
|
self.xbmcplayer = Player()
|
||||||
xbmc.Monitor.__init__(self)
|
self.playqueue = self.mgr.playqueue
|
||||||
|
Monitor.__init__(self)
|
||||||
log.info("Kodi monitor started.")
|
log.info("Kodi monitor started.")
|
||||||
|
|
||||||
def onScanStarted(self, library):
|
def onScanStarted(self, library):
|
||||||
|
@ -70,7 +70,7 @@ class KodiMonitor(xbmc.Monitor):
|
||||||
def onNotification(self, sender, method, data):
|
def onNotification(self, sender, method, data):
|
||||||
|
|
||||||
if data:
|
if data:
|
||||||
data = json.loads(data, 'utf-8')
|
data = loads(data, 'utf-8')
|
||||||
log.debug("Method: %s Data: %s" % (method, data))
|
log.debug("Method: %s Data: %s" % (method, data))
|
||||||
|
|
||||||
if method == "Player.OnPlay":
|
if method == "Player.OnPlay":
|
||||||
|
@ -92,18 +92,18 @@ class KodiMonitor(xbmc.Monitor):
|
||||||
log.info("Item is invalid for playstate update.")
|
log.info("Item is invalid for playstate update.")
|
||||||
else:
|
else:
|
||||||
# Send notification to the server.
|
# Send notification to the server.
|
||||||
with embydb.GetEmbyDB() as emby_db:
|
with plexdb.Get_Plex_DB() as plexcur:
|
||||||
emby_dbitem = emby_db.getItem_byKodiId(kodiid, item_type)
|
plex_dbitem = plexcur.getItem_byKodiId(kodiid, item_type)
|
||||||
try:
|
try:
|
||||||
itemid = emby_dbitem[0]
|
itemid = plex_dbitem[0]
|
||||||
except TypeError:
|
except TypeError:
|
||||||
log.error("Could not find itemid in emby database for a "
|
log.error("Could not find itemid in plex database for a "
|
||||||
"video library update")
|
"video library update")
|
||||||
else:
|
else:
|
||||||
# Stop from manually marking as watched unwatched, with actual playback.
|
# Stop from manually marking as watched unwatched, with actual playback.
|
||||||
if window('emby_skipWatched%s' % itemid) == "true":
|
if window('plex_skipWatched%s' % itemid) == "true":
|
||||||
# property is set in player.py
|
# property is set in player.py
|
||||||
window('emby_skipWatched%s' % itemid, clear=True)
|
window('plex_skipWatched%s' % itemid, clear=True)
|
||||||
else:
|
else:
|
||||||
# notify the server
|
# notify the server
|
||||||
if playcount != 0:
|
if playcount != 0:
|
||||||
|
@ -112,40 +112,7 @@ class KodiMonitor(xbmc.Monitor):
|
||||||
scrobble(itemid, 'unwatched')
|
scrobble(itemid, 'unwatched')
|
||||||
|
|
||||||
elif method == "VideoLibrary.OnRemove":
|
elif method == "VideoLibrary.OnRemove":
|
||||||
# Removed function, because with plugin paths + clean library, it will wipe
|
|
||||||
# entire library if user has permissions. Instead, use the emby context menu available
|
|
||||||
# in Isengard and higher version
|
|
||||||
pass
|
pass
|
||||||
'''try:
|
|
||||||
kodiid = data['id']
|
|
||||||
type = data['type']
|
|
||||||
except (KeyError, TypeError):
|
|
||||||
log.info("Item is invalid for emby deletion.")
|
|
||||||
else:
|
|
||||||
# Send the delete action to the server.
|
|
||||||
embyconn = utils.kodiSQL('emby')
|
|
||||||
embycursor = embyconn.cursor()
|
|
||||||
emby_db = embydb.Embydb_Functions(embycursor)
|
|
||||||
emby_dbitem = emby_db.getItem_byKodiId(kodiid, type)
|
|
||||||
try:
|
|
||||||
itemid = emby_dbitem[0]
|
|
||||||
except TypeError:
|
|
||||||
log.info("Could not find itemid in emby database.")
|
|
||||||
else:
|
|
||||||
if settings('skipContextMenu') != "true":
|
|
||||||
resp = xbmcgui.Dialog().yesno(
|
|
||||||
heading="Confirm delete",
|
|
||||||
line1="Delete file on Emby Server?")
|
|
||||||
if not resp:
|
|
||||||
log.info("User skipped deletion.")
|
|
||||||
embycursor.close()
|
|
||||||
return
|
|
||||||
|
|
||||||
url = "{server}/emby/Items/%s?format=json" % itemid
|
|
||||||
log.info("Deleting request: %s" % itemid)
|
|
||||||
doUtils.downloadUrl(url, action_type="DELETE")
|
|
||||||
finally:
|
|
||||||
embycursor.close()'''
|
|
||||||
|
|
||||||
elif method == "System.OnSleep":
|
elif method == "System.OnSleep":
|
||||||
# Connection is going to sleep
|
# Connection is going to sleep
|
||||||
|
@ -154,18 +121,15 @@ class KodiMonitor(xbmc.Monitor):
|
||||||
|
|
||||||
elif method == "System.OnWake":
|
elif method == "System.OnWake":
|
||||||
# Allow network to wake up
|
# Allow network to wake up
|
||||||
xbmc.sleep(10000)
|
sleep(10000)
|
||||||
window('plex_onWake', value="true")
|
window('plex_onWake', value="true")
|
||||||
window('plex_online', value="false")
|
window('plex_online', value="false")
|
||||||
|
|
||||||
elif method == "GUI.OnScreensaverDeactivated":
|
elif method == "GUI.OnScreensaverDeactivated":
|
||||||
if settings('dbSyncScreensaver') == "true":
|
if settings('dbSyncScreensaver') == "true":
|
||||||
xbmc.sleep(5000)
|
sleep(5000)
|
||||||
window('plex_runLibScan', value="full")
|
window('plex_runLibScan', value="full")
|
||||||
|
|
||||||
elif method == "Playlist.OnClear":
|
|
||||||
pass
|
|
||||||
|
|
||||||
def PlayBackStart(self, data):
|
def PlayBackStart(self, data):
|
||||||
"""
|
"""
|
||||||
Called whenever a playback is started
|
Called whenever a playback is started
|
||||||
|
@ -177,7 +141,7 @@ class KodiMonitor(xbmc.Monitor):
|
||||||
currentFile = None
|
currentFile = None
|
||||||
count = 0
|
count = 0
|
||||||
while currentFile is None:
|
while currentFile is None:
|
||||||
xbmc.sleep(100)
|
sleep(100)
|
||||||
try:
|
try:
|
||||||
currentFile = self.xbmcplayer.getPlayingFile()
|
currentFile = self.xbmcplayer.getPlayingFile()
|
||||||
except:
|
except:
|
||||||
|
@ -201,7 +165,7 @@ class KodiMonitor(xbmc.Monitor):
|
||||||
|
|
||||||
# Try to get a Kodi ID
|
# Try to get a Kodi ID
|
||||||
# If PKC was used - native paths, not direct paths
|
# If PKC was used - native paths, not direct paths
|
||||||
plexid = window('emby_%s.itemid' % tryEncode(currentFile))
|
plexid = window('plex_%s.itemid' % tryEncode(currentFile))
|
||||||
# Get rid of the '' if the window property was not set
|
# Get rid of the '' if the window property was not set
|
||||||
plexid = None if not plexid else plexid
|
plexid = None if not plexid else plexid
|
||||||
kodiid = None
|
kodiid = None
|
||||||
|
@ -215,27 +179,16 @@ class KodiMonitor(xbmc.Monitor):
|
||||||
# When using Widgets, Kodi doesn't tell us shit so we need this hack
|
# When using Widgets, Kodi doesn't tell us shit so we need this hack
|
||||||
if (kodiid is None and plexid is None and typus != 'song'
|
if (kodiid is None and plexid is None and typus != 'song'
|
||||||
and not currentFile.startswith('http')):
|
and not currentFile.startswith('http')):
|
||||||
try:
|
(kodiid, typus) = get_kodiid_from_filename(currentFile)
|
||||||
filename = currentFile.rsplit('/', 1)[1]
|
if kodiid is None:
|
||||||
path = currentFile.rsplit('/', 1)[0] + '/'
|
return
|
||||||
except IndexError:
|
|
||||||
filename = currentFile.rsplit('\\', 1)[1]
|
|
||||||
path = currentFile.rsplit('\\', 1)[0] + '\\'
|
|
||||||
log.debug('Trying to figure out playing item from filename: %s '
|
|
||||||
'and path: %s' % (filename, path))
|
|
||||||
with kodidb.GetKodiDB('video') as kodi_db:
|
|
||||||
try:
|
|
||||||
kodiid, typus = kodi_db.getIdFromFilename(filename, path)
|
|
||||||
except TypeError:
|
|
||||||
log.info('Abort playback report, could not id kodi item')
|
|
||||||
return
|
|
||||||
|
|
||||||
if plexid is None:
|
if plexid is None:
|
||||||
# Get Plex' item id
|
# Get Plex' item id
|
||||||
with embydb.GetEmbyDB() as emby_db:
|
with plexdb.Get_Plex_DB() as plexcursor:
|
||||||
emby_dbitem = emby_db.getItem_byKodiId(kodiid, typus)
|
plex_dbitem = plexcursor.getItem_byKodiId(kodiid, typus)
|
||||||
try:
|
try:
|
||||||
plexid = emby_dbitem[0]
|
plexid = plex_dbitem[0]
|
||||||
except TypeError:
|
except TypeError:
|
||||||
log.info("No Plex id returned for kodiid %s. Aborting playback"
|
log.info("No Plex id returned for kodiid %s. Aborting playback"
|
||||||
" report" % kodiid)
|
" report" % kodiid)
|
||||||
|
@ -256,24 +209,25 @@ class KodiMonitor(xbmc.Monitor):
|
||||||
# Save currentFile for cleanup later and to be able to access refs
|
# Save currentFile for cleanup later and to be able to access refs
|
||||||
window('plex_lastPlayedFiled', value=currentFile)
|
window('plex_lastPlayedFiled', value=currentFile)
|
||||||
window('plex_currently_playing_itemid', value=plexid)
|
window('plex_currently_playing_itemid', value=plexid)
|
||||||
window("emby_%s.itemid" % tryEncode(currentFile), value=plexid)
|
window("plex_%s.itemid" % tryEncode(currentFile), value=plexid)
|
||||||
log.info('Finish playback startup')
|
log.info('Finish playback startup')
|
||||||
|
|
||||||
def StartDirectPath(self, plexid, type, currentFile):
|
def StartDirectPath(self, plexid, type, currentFile):
|
||||||
"""
|
"""
|
||||||
Set some additional stuff if playback was initiated by Kodi, not PKC
|
Set some additional stuff if playback was initiated by Kodi, not PKC
|
||||||
"""
|
"""
|
||||||
result = self.doUtils('{server}/library/metadata/%s' % plexid)
|
xml = self.doUtils('{server}/library/metadata/%s' % plexid)
|
||||||
try:
|
try:
|
||||||
result[0].attrib
|
xml[0].attrib
|
||||||
except:
|
except:
|
||||||
log.error('Did not receive a valid XML for plexid %s.' % plexid)
|
log.error('Did not receive a valid XML for plexid %s.' % plexid)
|
||||||
return False
|
return False
|
||||||
# Setup stuff, because playback was started by Kodi, not PKC
|
# Setup stuff, because playback was started by Kodi, not PKC
|
||||||
pbutils.PlaybackUtils(result[0]).setProperties(
|
api = API(xml[0])
|
||||||
currentFile, xbmcgui.ListItem())
|
listitem = api.CreateListItemFromPlexItem()
|
||||||
|
api.set_playback_win_props(currentFile, listitem)
|
||||||
if type == "song" and settings('streamMusic') == "true":
|
if type == "song" and settings('streamMusic') == "true":
|
||||||
window('emby_%s.playmethod' % currentFile, value="DirectStream")
|
window('plex_%s.playmethod' % currentFile, value="DirectStream")
|
||||||
else:
|
else:
|
||||||
window('emby_%s.playmethod' % currentFile, value="DirectPlay")
|
window('plex_%s.playmethod' % currentFile, value="DirectPlay")
|
||||||
log.debug('Window properties set for direct paths!')
|
log.debug('Window properties set for direct paths!')
|
||||||
|
|
|
@ -11,7 +11,7 @@ import xbmc
|
||||||
import xbmcgui
|
import xbmcgui
|
||||||
import xbmcvfs
|
import xbmcvfs
|
||||||
|
|
||||||
from utils import window, settings, getUnixTimestamp, kodiSQL, sourcesXML,\
|
from utils import window, settings, getUnixTimestamp, sourcesXML,\
|
||||||
ThreadMethods, ThreadMethodsAdditionalStop, LogTime, getScreensaver,\
|
ThreadMethods, ThreadMethodsAdditionalStop, LogTime, getScreensaver,\
|
||||||
setScreensaver, playlistXSP, language as lang, DateToKodi, reset,\
|
setScreensaver, playlistXSP, language as lang, DateToKodi, reset,\
|
||||||
advancedSettingsXML, getKodiVideoDBPath, tryDecode, deletePlaylists,\
|
advancedSettingsXML, getKodiVideoDBPath, tryDecode, deletePlaylists,\
|
||||||
|
@ -19,7 +19,7 @@ from utils import window, settings, getUnixTimestamp, kodiSQL, sourcesXML,\
|
||||||
import clientinfo
|
import clientinfo
|
||||||
import downloadutils
|
import downloadutils
|
||||||
import itemtypes
|
import itemtypes
|
||||||
import embydb_functions as embydb
|
import plexdb_functions as plexdb
|
||||||
import kodidb_functions as kodidb
|
import kodidb_functions as kodidb
|
||||||
import userclient
|
import userclient
|
||||||
import videonodes
|
import videonodes
|
||||||
|
@ -302,9 +302,9 @@ class ProcessFanartThread(Thread):
|
||||||
# Leave the Plex art untouched
|
# Leave the Plex art untouched
|
||||||
allartworks = None
|
allartworks = None
|
||||||
else:
|
else:
|
||||||
with embydb.GetEmbyDB() as emby_db:
|
with plexdb.Get_Plex_DB() as plex_db:
|
||||||
try:
|
try:
|
||||||
kodiId = emby_db.getItem_byId(item['itemId'])[0]
|
kodiId = plex_db.getItem_byId(item['itemId'])[0]
|
||||||
except TypeError:
|
except TypeError:
|
||||||
log.error('Could not get Kodi id for plex id %s'
|
log.error('Could not get Kodi id for plex id %s'
|
||||||
% item['itemId'])
|
% item['itemId'])
|
||||||
|
@ -356,19 +356,10 @@ class ProcessFanartThread(Thread):
|
||||||
@ThreadMethods
|
@ThreadMethods
|
||||||
class LibrarySync(Thread):
|
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!
|
def __init__(self, callback=None):
|
||||||
_shared_state = {}
|
self.mgr = callback
|
||||||
|
|
||||||
def __init__(self, queue):
|
|
||||||
self.__dict__ = self._shared_state
|
|
||||||
|
|
||||||
# Communication with websockets
|
|
||||||
self.queue = queue
|
|
||||||
self.itemsToProcess = []
|
self.itemsToProcess = []
|
||||||
self.sessionKeys = []
|
self.sessionKeys = []
|
||||||
self.fanartqueue = Queue.Queue()
|
self.fanartqueue = Queue.Queue()
|
||||||
|
@ -455,7 +446,9 @@ class LibrarySync(Thread):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
plexId = None
|
plexId = None
|
||||||
for mediatype in ('movie', 'show', 'artist'):
|
for mediatype in (PF.PLEX_TYPE_MOVIE,
|
||||||
|
PF.PLEX_TYPE_SHOW,
|
||||||
|
PF.PLEX_TYPE_ARTIST):
|
||||||
if plexId is not None:
|
if plexId is not None:
|
||||||
break
|
break
|
||||||
for view in sections:
|
for view in sections:
|
||||||
|
@ -539,24 +532,34 @@ class LibrarySync(Thread):
|
||||||
|
|
||||||
def initializeDBs(self):
|
def initializeDBs(self):
|
||||||
"""
|
"""
|
||||||
Run once during startup to verify that emby db exists.
|
Run once during startup to verify that plex db exists.
|
||||||
"""
|
"""
|
||||||
embyconn = kodiSQL('emby')
|
with plexdb.Get_Plex_DB() as plex_db:
|
||||||
embycursor = embyconn.cursor()
|
# Create the tables for the plex database
|
||||||
# Create the tables for the emby database
|
plex_db.plexcursor.execute('''
|
||||||
# emby, view, version
|
CREATE TABLE IF NOT EXISTS plex(
|
||||||
embycursor.execute(
|
plex_id TEXT UNIQUE,
|
||||||
"""CREATE TABLE IF NOT EXISTS emby(
|
view_id TEXT,
|
||||||
emby_id TEXT UNIQUE, media_folder TEXT, emby_type TEXT, media_type TEXT, kodi_id INTEGER,
|
plex_type TEXT,
|
||||||
kodi_fileid INTEGER, kodi_pathid INTEGER, parent_id INTEGER, checksum INTEGER)""")
|
kodi_type TEXT,
|
||||||
embycursor.execute(
|
kodi_id INTEGER,
|
||||||
"""CREATE TABLE IF NOT EXISTS view(
|
kodi_fileid INTEGER,
|
||||||
view_id TEXT UNIQUE, view_name TEXT, media_type TEXT, kodi_tagid INTEGER)""")
|
kodi_pathid INTEGER,
|
||||||
embycursor.execute("CREATE TABLE IF NOT EXISTS version(idVersion TEXT)")
|
parent_id INTEGER,
|
||||||
embyconn.commit()
|
checksum INTEGER)
|
||||||
|
''')
|
||||||
# content sync: movies, tvshows, musicvideos, music
|
plex_db.plexcursor.execute('''
|
||||||
embyconn.close()
|
CREATE TABLE IF NOT EXISTS view(
|
||||||
|
view_id TEXT UNIQUE,
|
||||||
|
view_name TEXT,
|
||||||
|
kodi_type TEXT,
|
||||||
|
kodi_tagid INTEGER)
|
||||||
|
''')
|
||||||
|
plex_db.plexcursor.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS version(idVersion TEXT)
|
||||||
|
''')
|
||||||
|
# Create an index for actors to speed up sync
|
||||||
|
create_actor_db_index()
|
||||||
|
|
||||||
# Create an index for actors to speed up sync
|
# Create an index for actors to speed up sync
|
||||||
create_actor_db_index()
|
create_actor_db_index()
|
||||||
|
@ -643,12 +646,13 @@ class LibrarySync(Thread):
|
||||||
log.error('Path hack failed with error message: %s' % str(e))
|
log.error('Path hack failed with error message: %s' % str(e))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def processView(self, folderItem, kodi_db, emby_db, totalnodes):
|
def processView(self, folderItem, kodi_db, plex_db, totalnodes):
|
||||||
vnodes = self.vnodes
|
vnodes = self.vnodes
|
||||||
folder = folderItem.attrib
|
folder = folderItem.attrib
|
||||||
mediatype = folder['type']
|
mediatype = folder['type']
|
||||||
# Only process supported formats
|
# Only process supported formats
|
||||||
if mediatype not in ('movie', 'show', 'artist', 'photo'):
|
if mediatype not in (PF.PLEX_TYPE_MOVIE, PF.PLEX_TYPE_SHOW,
|
||||||
|
PF.PLEX_TYPE_ARTIST, PF.PLEX_TYPE_PHOTO):
|
||||||
return totalnodes
|
return totalnodes
|
||||||
|
|
||||||
# Prevent duplicate for nodes of the same type
|
# Prevent duplicate for nodes of the same type
|
||||||
|
@ -661,8 +665,8 @@ class LibrarySync(Thread):
|
||||||
foldername = folder['title']
|
foldername = folder['title']
|
||||||
viewtype = folder['type']
|
viewtype = folder['type']
|
||||||
|
|
||||||
# Get current media folders from emby database
|
# Get current media folders from plex database
|
||||||
view = emby_db.getView_byId(folderid)
|
view = plex_db.getView_byId(folderid)
|
||||||
try:
|
try:
|
||||||
current_viewname = view[0]
|
current_viewname = view[0]
|
||||||
current_viewtype = view[1]
|
current_viewtype = view[1]
|
||||||
|
@ -672,12 +676,12 @@ class LibrarySync(Thread):
|
||||||
tagid = kodi_db.createTag(foldername)
|
tagid = kodi_db.createTag(foldername)
|
||||||
# Create playlist for the video library
|
# Create playlist for the video library
|
||||||
if (foldername not in playlists and
|
if (foldername not in playlists and
|
||||||
mediatype in ('movie', 'show', 'musicvideos')):
|
mediatype in (PF.PLEX_TYPE_MOVIE, PF.PLEX_TYPE_SHOW)):
|
||||||
playlistXSP(mediatype, foldername, folderid, viewtype)
|
playlistXSP(mediatype, foldername, folderid, viewtype)
|
||||||
playlists.append(foldername)
|
playlists.append(foldername)
|
||||||
# Create the video node
|
# Create the video node
|
||||||
if (foldername not in nodes and
|
if (foldername not in nodes and
|
||||||
mediatype not in ("musicvideos", "artist")):
|
mediatype != PF.PLEX_TYPE_ARTIST):
|
||||||
vnodes.viewNode(sorted_views.index(foldername),
|
vnodes.viewNode(sorted_views.index(foldername),
|
||||||
foldername,
|
foldername,
|
||||||
mediatype,
|
mediatype,
|
||||||
|
@ -685,8 +689,8 @@ class LibrarySync(Thread):
|
||||||
folderid)
|
folderid)
|
||||||
nodes.append(foldername)
|
nodes.append(foldername)
|
||||||
totalnodes += 1
|
totalnodes += 1
|
||||||
# Add view to emby database
|
# Add view to plex database
|
||||||
emby_db.addView(folderid, foldername, viewtype, tagid)
|
plex_db.addView(folderid, foldername, viewtype, tagid)
|
||||||
else:
|
else:
|
||||||
log.info(' '.join((
|
log.info(' '.join((
|
||||||
"Found viewid: %s" % folderid,
|
"Found viewid: %s" % folderid,
|
||||||
|
@ -708,10 +712,10 @@ class LibrarySync(Thread):
|
||||||
tagid = kodi_db.createTag(foldername)
|
tagid = kodi_db.createTag(foldername)
|
||||||
|
|
||||||
# Update view with new info
|
# Update view with new info
|
||||||
emby_db.updateView(foldername, tagid, folderid)
|
plex_db.updateView(foldername, tagid, folderid)
|
||||||
|
|
||||||
if mediatype != "artist":
|
if mediatype != "artist":
|
||||||
if emby_db.getView_byName(current_viewname) is None:
|
if plex_db.getView_byName(current_viewname) is None:
|
||||||
# The tag could be a combined view. Ensure there's
|
# The tag could be a combined view. Ensure there's
|
||||||
# no other tags with the same name before deleting
|
# no other tags with the same name before deleting
|
||||||
# playlist.
|
# playlist.
|
||||||
|
@ -731,7 +735,7 @@ class LibrarySync(Thread):
|
||||||
delete=True)
|
delete=True)
|
||||||
# Added new playlist
|
# Added new playlist
|
||||||
if (foldername not in playlists and
|
if (foldername not in playlists and
|
||||||
mediatype in ('movie', 'show', 'musicvideos')):
|
mediatype in (PF.PLEX_TYPE_MOVIE, PF.PLEX_TYPE_SHOW)):
|
||||||
playlistXSP(mediatype,
|
playlistXSP(mediatype,
|
||||||
foldername,
|
foldername,
|
||||||
folderid,
|
folderid,
|
||||||
|
@ -748,16 +752,16 @@ class LibrarySync(Thread):
|
||||||
totalnodes += 1
|
totalnodes += 1
|
||||||
|
|
||||||
# Update items with new tag
|
# Update items with new tag
|
||||||
items = emby_db.getItem_byView(folderid)
|
items = plex_db.getItem_byView(folderid)
|
||||||
for item in items:
|
for item in items:
|
||||||
# Remove the "s" from viewtype for tags
|
# Remove the "s" from viewtype for tags
|
||||||
kodi_db.updateTag(
|
kodi_db.updateTag(
|
||||||
current_tagid, tagid, item[0], current_viewtype[:-1])
|
current_tagid, tagid, item[0], current_viewtype[:-1])
|
||||||
else:
|
else:
|
||||||
# Validate the playlist exists or recreate it
|
# Validate the playlist exists or recreate it
|
||||||
if mediatype != "artist":
|
if mediatype != PF.PLEX_TYPE_ARTIST:
|
||||||
if (foldername not in playlists and
|
if (foldername not in playlists and
|
||||||
mediatype in ('movie', 'show', 'musicvideos')):
|
mediatype in (PF.PLEX_TYPE_MOVIE, PF.PLEX_TYPE_SHOW)):
|
||||||
playlistXSP(mediatype,
|
playlistXSP(mediatype,
|
||||||
foldername,
|
foldername,
|
||||||
folderid,
|
folderid,
|
||||||
|
@ -792,22 +796,22 @@ class LibrarySync(Thread):
|
||||||
|
|
||||||
# For whatever freaking reason, .copy() or dict() does NOT work?!?!?!
|
# For whatever freaking reason, .copy() or dict() does NOT work?!?!?!
|
||||||
self.nodes = {
|
self.nodes = {
|
||||||
'movie': [],
|
PF.PLEX_TYPE_MOVIE: [],
|
||||||
'show': [],
|
PF.PLEX_TYPE_SHOW: [],
|
||||||
'artist': [],
|
PF.PLEX_TYPE_ARTIST: [],
|
||||||
'photo': []
|
PF.PLEX_TYPE_PHOTO: []
|
||||||
}
|
}
|
||||||
self.playlists = {
|
self.playlists = {
|
||||||
'movie': [],
|
PF.PLEX_TYPE_MOVIE: [],
|
||||||
'show': [],
|
PF.PLEX_TYPE_SHOW: [],
|
||||||
'artist': [],
|
PF.PLEX_TYPE_ARTIST: [],
|
||||||
'photo': []
|
PF.PLEX_TYPE_PHOTO: []
|
||||||
}
|
}
|
||||||
self.sorted_views = []
|
self.sorted_views = []
|
||||||
|
|
||||||
for view in sections:
|
for view in sections:
|
||||||
itemType = view.attrib['type']
|
itemType = view.attrib['type']
|
||||||
if itemType in ('movie', 'show', 'photo'): # NOT artist for now
|
if itemType in (PF.PLEX_TYPE_MOVIE, PF.PLEX_TYPE_SHOW, PF.PLEX_TYPE_PHOTO): # NOT artist for now
|
||||||
self.sorted_views.append(view.attrib['title'])
|
self.sorted_views.append(view.attrib['title'])
|
||||||
log.debug('Sorted views: %s' % self.sorted_views)
|
log.debug('Sorted views: %s' % self.sorted_views)
|
||||||
|
|
||||||
|
@ -815,15 +819,15 @@ class LibrarySync(Thread):
|
||||||
vnodes.clearProperties()
|
vnodes.clearProperties()
|
||||||
totalnodes = len(self.sorted_views)
|
totalnodes = len(self.sorted_views)
|
||||||
|
|
||||||
with embydb.GetEmbyDB() as emby_db:
|
with plexdb.Get_Plex_DB() as plex_db:
|
||||||
# Backup old views to delete them later, if needed (at the end
|
# Backup old views to delete them later, if needed (at the end
|
||||||
# of this method, only unused views will be left in oldviews)
|
# of this method, only unused views will be left in oldviews)
|
||||||
self.old_views = emby_db.getViews()
|
self.old_views = plex_db.getViews()
|
||||||
with kodidb.GetKodiDB('video') as kodi_db:
|
with kodidb.GetKodiDB('video') as kodi_db:
|
||||||
for folderItem in sections:
|
for folderItem in sections:
|
||||||
totalnodes = self.processView(folderItem,
|
totalnodes = self.processView(folderItem,
|
||||||
kodi_db,
|
kodi_db,
|
||||||
emby_db,
|
plex_db,
|
||||||
totalnodes)
|
totalnodes)
|
||||||
# Add video nodes listings
|
# Add video nodes listings
|
||||||
# Plex: there seem to be no favorites/favorites tag
|
# Plex: there seem to be no favorites/favorites tag
|
||||||
|
@ -842,19 +846,17 @@ class LibrarySync(Thread):
|
||||||
# "movies",
|
# "movies",
|
||||||
# "channels")
|
# "channels")
|
||||||
# totalnodes += 1
|
# totalnodes += 1
|
||||||
with kodidb.GetKodiDB('music') as kodi_db:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Save total
|
# Save total
|
||||||
window('Plex.nodes.total', str(totalnodes))
|
window('Plex.nodes.total', str(totalnodes))
|
||||||
|
|
||||||
# Reopen DB connection to ensure that changes were commited before
|
# Reopen DB connection to ensure that changes were commited before
|
||||||
with embydb.GetEmbyDB() as emby_db:
|
with plexdb.Get_Plex_DB() as plex_db:
|
||||||
log.info("Removing views: %s" % self.old_views)
|
log.info("Removing views: %s" % self.old_views)
|
||||||
for view in self.old_views:
|
for view in self.old_views:
|
||||||
emby_db.removeView(view)
|
plex_db.removeView(view)
|
||||||
# update views for all:
|
# update views for all:
|
||||||
self.views = emby_db.getAllViewInfo()
|
self.views = plex_db.getAllViewInfo()
|
||||||
|
|
||||||
log.info("Finished processing views. Views saved: %s" % self.views)
|
log.info("Finished processing views. Views saved: %s" % self.views)
|
||||||
return True
|
return True
|
||||||
|
@ -1038,9 +1040,10 @@ class LibrarySync(Thread):
|
||||||
if (settings('FanartTV') == 'true' and
|
if (settings('FanartTV') == 'true' and
|
||||||
itemType in ('Movies', 'TVShows')):
|
itemType in ('Movies', 'TVShows')):
|
||||||
# Save to queue for later processing
|
# Save to queue for later processing
|
||||||
typus = {'Movies': 'movie', 'TVShows': 'tvshow'}[itemType]
|
typus = {'Movies': PF.KODI_TYPE_MOVIE,
|
||||||
|
'TVShows': PF.KODI_TYPE_SHOW}[itemType]
|
||||||
for item in self.updatelist:
|
for item in self.updatelist:
|
||||||
if item['mediaType'] in ('movie', 'show'):
|
if item['mediaType'] in (PF.KODI_TYPE_MOVIE, PF.KODI_TYPE_SHOW):
|
||||||
self.fanartqueue.put({
|
self.fanartqueue.put({
|
||||||
'itemId': item['itemId'],
|
'itemId': item['itemId'],
|
||||||
'class': itemType,
|
'class': itemType,
|
||||||
|
@ -1056,16 +1059,17 @@ class LibrarySync(Thread):
|
||||||
|
|
||||||
itemType = 'Movies'
|
itemType = 'Movies'
|
||||||
|
|
||||||
views = [x for x in self.views if x['itemtype'] == 'movie']
|
views = [x for x in self.views if x['itemtype'] == PF.KODI_TYPE_MOVIE]
|
||||||
log.info("Processing Plex %s. Libraries: %s" % (itemType, views))
|
log.info("Processing Plex %s. Libraries: %s" % (itemType, views))
|
||||||
|
|
||||||
self.allKodiElementsId = {}
|
self.allKodiElementsId = {}
|
||||||
if self.compare:
|
if self.compare:
|
||||||
with embydb.GetEmbyDB() as emby_db:
|
with plexdb.Get_Plex_DB() as plex_db:
|
||||||
# Get movies from Plex server
|
# Get movies from Plex server
|
||||||
# Pull the list of movies and boxsets in Kodi
|
# Pull the list of movies and boxsets in Kodi
|
||||||
try:
|
try:
|
||||||
self.allKodiElementsId = dict(emby_db.getChecksum('Movie'))
|
self.allKodiElementsId = dict(
|
||||||
|
plex_db.getChecksum(PF.PLEX_TYPE_MOVIE))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
self.allKodiElementsId = {}
|
self.allKodiElementsId = {}
|
||||||
|
|
||||||
|
@ -1148,11 +1152,13 @@ class LibrarySync(Thread):
|
||||||
|
|
||||||
self.allKodiElementsId = {}
|
self.allKodiElementsId = {}
|
||||||
if self.compare:
|
if self.compare:
|
||||||
with embydb.GetEmbyDB() as emby_db:
|
with plexdb.Get_Plex_DB() as plex:
|
||||||
# Pull the list of TV shows already in Kodi
|
# Pull the list of TV shows already in Kodi
|
||||||
for kind in ('Series', 'Season', 'Episode'):
|
for kind in (PF.PLEX_TYPE_SHOW,
|
||||||
|
PF.PLEX_TYPE_SEASON,
|
||||||
|
PF.PLEX_TYPE_EPISODE):
|
||||||
try:
|
try:
|
||||||
elements = dict(emby_db.getChecksum(kind))
|
elements = dict(plex.getChecksum(kind))
|
||||||
self.allKodiElementsId.update(elements)
|
self.allKodiElementsId.update(elements)
|
||||||
# Yet empty/not yet synched
|
# Yet empty/not yet synched
|
||||||
except ValueError:
|
except ValueError:
|
||||||
|
@ -1270,22 +1276,24 @@ class LibrarySync(Thread):
|
||||||
def PlexMusic(self):
|
def PlexMusic(self):
|
||||||
itemType = 'Music'
|
itemType = 'Music'
|
||||||
|
|
||||||
views = [x for x in self.views if x['itemtype'] == 'artist']
|
views = [x for x in self.views if x['itemtype'] == PF.PLEX_TYPE_ARTIST]
|
||||||
log.info("Media folders for %s: %s" % (itemType, views))
|
log.info("Media folders for %s: %s" % (itemType, views))
|
||||||
|
|
||||||
methods = {
|
methods = {
|
||||||
'MusicArtist': 'add_updateArtist',
|
PF.PLEX_TYPE_ARTIST: 'add_updateArtist',
|
||||||
'MusicAlbum': 'add_updateAlbum',
|
PF.PLEX_TYPE_ALBUM: 'add_updateAlbum',
|
||||||
'Audio': 'add_updateSong'
|
PF.PLEX_TYPE_SONG: 'add_updateSong'
|
||||||
}
|
}
|
||||||
urlArgs = {
|
urlArgs = {
|
||||||
'MusicArtist': {'type': 8},
|
PF.PLEX_TYPE_ARTIST: {'type': 8},
|
||||||
'MusicAlbum': {'type': 9},
|
PF.PLEX_TYPE_ALBUM: {'type': 9},
|
||||||
'Audio': {'type': 10}
|
PF.PLEX_TYPE_SONG: {'type': 10}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Process artist, then album and tracks last to minimize overhead
|
# Process artist, then album and tracks last to minimize overhead
|
||||||
for kind in ('MusicArtist', 'MusicAlbum', 'Audio'):
|
for kind in (PF.PLEX_TYPE_ARTIST,
|
||||||
|
PF.PLEX_TYPE_ALBUM,
|
||||||
|
PF.PLEX_TYPE_SONG):
|
||||||
if self.threadStopped():
|
if self.threadStopped():
|
||||||
return False
|
return False
|
||||||
log.debug("Start processing music %s" % kind)
|
log.debug("Start processing music %s" % kind)
|
||||||
|
@ -1318,10 +1326,10 @@ class LibrarySync(Thread):
|
||||||
|
|
||||||
# Get a list of items already existing in Kodi db
|
# Get a list of items already existing in Kodi db
|
||||||
if self.compare:
|
if self.compare:
|
||||||
with embydb.GetEmbyDB() as emby_db:
|
with plexdb.Get_Plex_DB() as plex_db:
|
||||||
# Pull the list of items already in Kodi
|
# Pull the list of items already in Kodi
|
||||||
try:
|
try:
|
||||||
elements = dict(emby_db.getChecksum(kind))
|
elements = dict(plex_db.getChecksum(kind))
|
||||||
self.allKodiElementsId.update(elements)
|
self.allKodiElementsId.update(elements)
|
||||||
# Yet empty/nothing yet synched
|
# Yet empty/nothing yet synched
|
||||||
except ValueError:
|
except ValueError:
|
||||||
|
@ -1569,14 +1577,14 @@ class LibrarySync(Thread):
|
||||||
where
|
where
|
||||||
"""
|
"""
|
||||||
items = []
|
items = []
|
||||||
with embydb.GetEmbyDB() as emby_db:
|
with plexdb.Get_Plex_DB() as plex_db:
|
||||||
for item in data:
|
for item in data:
|
||||||
# Drop buffering messages immediately
|
# Drop buffering messages immediately
|
||||||
state = item.get('state')
|
state = item.get('state')
|
||||||
if state == 'buffering':
|
if state == 'buffering':
|
||||||
continue
|
continue
|
||||||
ratingKey = item.get('ratingKey')
|
ratingKey = item.get('ratingKey')
|
||||||
kodiInfo = emby_db.getItem_byId(ratingKey)
|
kodiInfo = plex_db.getItem_byId(ratingKey)
|
||||||
if kodiInfo is None:
|
if kodiInfo is None:
|
||||||
# Item not (yet) in Kodi library
|
# Item not (yet) in Kodi library
|
||||||
continue
|
continue
|
||||||
|
@ -1663,7 +1671,7 @@ class LibrarySync(Thread):
|
||||||
# Now tell Kodi where we are
|
# Now tell Kodi where we are
|
||||||
for item in items:
|
for item in items:
|
||||||
itemFkt = getattr(itemtypes,
|
itemFkt = getattr(itemtypes,
|
||||||
PF.ITEMTYPE_FROM_PLEXTYPE[item['kodi_type']])
|
PF.ITEMTYPE_FROM_KODITYPE[item['kodi_type']])
|
||||||
with itemFkt() as Fkt:
|
with itemFkt() as Fkt:
|
||||||
Fkt.updatePlaystate(item)
|
Fkt.updatePlaystate(item)
|
||||||
|
|
||||||
|
@ -1675,12 +1683,12 @@ class LibrarySync(Thread):
|
||||||
"""
|
"""
|
||||||
items = []
|
items = []
|
||||||
typus = {
|
typus = {
|
||||||
'Movie': 'Movies',
|
PF.PLEX_TYPE_MOVIE: 'Movies',
|
||||||
'Series': 'TVShows'
|
PF.PLEX_TYPE_SHOW: 'TVShows'
|
||||||
}
|
}
|
||||||
with embydb.GetEmbyDB() as emby_db:
|
with plexdb.Get_Plex_DB() as plex_db:
|
||||||
for plextype in typus:
|
for plextype in typus:
|
||||||
items.extend(emby_db.itemsByType(plextype))
|
items.extend(plex_db.itemsByType(plextype))
|
||||||
# Shuffle the list to not always start out identically
|
# Shuffle the list to not always start out identically
|
||||||
shuffle(items)
|
shuffle(items)
|
||||||
for item in items:
|
for item in items:
|
||||||
|
@ -1720,7 +1728,8 @@ class LibrarySync(Thread):
|
||||||
|
|
||||||
xbmcplayer = xbmc.Player()
|
xbmcplayer = xbmc.Player()
|
||||||
|
|
||||||
queue = self.queue
|
# Link to Websocket queue
|
||||||
|
queue = self.mgr.ws.queue
|
||||||
|
|
||||||
startupComplete = False
|
startupComplete = False
|
||||||
self.views = []
|
self.views = []
|
||||||
|
|
41
resources/lib/monitor_kodi_play.py
Normal file
41
resources/lib/monitor_kodi_play.py
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
###############################################################################
|
||||||
|
import logging
|
||||||
|
from threading import Thread
|
||||||
|
from Queue import Queue
|
||||||
|
|
||||||
|
from xbmc import sleep
|
||||||
|
|
||||||
|
from utils import window, ThreadMethods
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
log = logging.getLogger("PLEX."+__name__)
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
|
||||||
|
@ThreadMethods
|
||||||
|
class Monitor_Kodi_Play(Thread):
|
||||||
|
"""
|
||||||
|
Monitors for new plays initiated on the Kodi side with addon paths.
|
||||||
|
Immediately throws them into a queue to be processed by playback_starter
|
||||||
|
"""
|
||||||
|
# Borg - multiple instances, shared state
|
||||||
|
def __init__(self, callback=None):
|
||||||
|
self.mgr = callback
|
||||||
|
self.playback_queue = Queue()
|
||||||
|
Thread.__init__(self)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
threadStopped = self.threadStopped
|
||||||
|
queue = self.playback_queue
|
||||||
|
log.info("----===## Starting Kodi_Play_Client ##===----")
|
||||||
|
while not threadStopped():
|
||||||
|
if window('plex_play_new_item'):
|
||||||
|
queue.put(window('plex_play_new_item'))
|
||||||
|
window('plex_play_new_item', clear=True)
|
||||||
|
else:
|
||||||
|
sleep(20)
|
||||||
|
# Put one last item into the queue to let playback_starter end
|
||||||
|
queue.put(None)
|
||||||
|
log.info("----===## Kodi_Play_Client stopped ##===----")
|
44
resources/lib/pickler.py
Normal file
44
resources/lib/pickler.py
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
###############################################################################
|
||||||
|
import logging
|
||||||
|
import cPickle as Pickle
|
||||||
|
|
||||||
|
from utils import pickl_window
|
||||||
|
###############################################################################
|
||||||
|
log = logging.getLogger("PLEX."+__name__)
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
|
||||||
|
def pickle_me(obj, window_var='plex_result'):
|
||||||
|
"""
|
||||||
|
Pickles the obj to the window variable. Use to transfer Python
|
||||||
|
objects between different PKC python instances (e.g. if default.py is
|
||||||
|
called and you'd want to use the service.py instance)
|
||||||
|
|
||||||
|
obj can be pretty much any Python object. However, classes and
|
||||||
|
functions won't work. See the Pickle documentation
|
||||||
|
"""
|
||||||
|
log.debug('Start pickling: %s' % obj)
|
||||||
|
pickl_window(window_var, value=Pickle.dumps(obj))
|
||||||
|
log.debug('Successfully pickled')
|
||||||
|
|
||||||
|
|
||||||
|
def unpickle_me(window_var='plex_result'):
|
||||||
|
"""
|
||||||
|
Unpickles a Python object from the window variable window_var.
|
||||||
|
Will then clear the window variable!
|
||||||
|
"""
|
||||||
|
result = pickl_window(window_var)
|
||||||
|
pickl_window(window_var, clear=True)
|
||||||
|
log.debug('Start unpickling')
|
||||||
|
obj = Pickle.loads(result)
|
||||||
|
log.debug('Successfully unpickled: %s' % obj)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
class Playback_Successful(object):
|
||||||
|
"""
|
||||||
|
Used to communicate with another PKC Python instance
|
||||||
|
"""
|
||||||
|
listitem = None
|
100
resources/lib/playback_starter.py
Normal file
100
resources/lib/playback_starter.py
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
###############################################################################
|
||||||
|
import logging
|
||||||
|
from threading import Thread
|
||||||
|
from urlparse import parse_qsl
|
||||||
|
|
||||||
|
from PKC_listitem import PKC_ListItem
|
||||||
|
from pickler import pickle_me, Playback_Successful
|
||||||
|
from playbackutils import PlaybackUtils
|
||||||
|
from utils import window
|
||||||
|
from PlexFunctions import GetPlexMetadata, PLEX_TYPE_PHOTO, \
|
||||||
|
KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE
|
||||||
|
from PlexAPI import API
|
||||||
|
from playqueue import lock
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
log = logging.getLogger("PLEX."+__name__)
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
|
||||||
|
class Playback_Starter(Thread):
|
||||||
|
"""
|
||||||
|
Processes new plays
|
||||||
|
"""
|
||||||
|
def __init__(self, callback=None):
|
||||||
|
self.mgr = callback
|
||||||
|
self.playqueue = self.mgr.playqueue
|
||||||
|
Thread.__init__(self)
|
||||||
|
|
||||||
|
def process_play(self, plex_id, kodi_id=None):
|
||||||
|
"""
|
||||||
|
Processes Kodi playback init for ONE item
|
||||||
|
"""
|
||||||
|
log.info("Process_play called with plex_id %s, kodi_id %s"
|
||||||
|
% (plex_id, kodi_id))
|
||||||
|
if window('plex_authenticated') != "true":
|
||||||
|
log.error('Not yet authenticated for PMS, abort starting playback')
|
||||||
|
# Todo: Warn user with dialog
|
||||||
|
return
|
||||||
|
xml = GetPlexMetadata(plex_id)
|
||||||
|
try:
|
||||||
|
xml[0].attrib
|
||||||
|
except (TypeError, AttributeError):
|
||||||
|
log.error('Could not get a PMS xml for plex id %s' % plex_id)
|
||||||
|
return
|
||||||
|
api = API(xml[0])
|
||||||
|
if api.getType() == PLEX_TYPE_PHOTO:
|
||||||
|
# Photo
|
||||||
|
result = Playback_Successful()
|
||||||
|
listitem = PKC_ListItem()
|
||||||
|
listitem = api.CreateListItemFromPlexItem(listitem)
|
||||||
|
api.AddStreamInfo(listitem)
|
||||||
|
api.set_listitem_artwork(listitem)
|
||||||
|
result.listitem = listitem
|
||||||
|
else:
|
||||||
|
# Video and Music
|
||||||
|
playqueue = self.playqueue.get_playqueue_from_type(
|
||||||
|
KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.getType()])
|
||||||
|
with lock:
|
||||||
|
result = PlaybackUtils(xml, playqueue).play(
|
||||||
|
plex_id,
|
||||||
|
kodi_id,
|
||||||
|
xml.attrib.get('librarySectionUUID'))
|
||||||
|
log.info('Done process_play, playqueues: %s'
|
||||||
|
% self.playqueue.playqueues)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def triage(self, item):
|
||||||
|
mode, params = item.split('?', 1)
|
||||||
|
params = dict(parse_qsl(params))
|
||||||
|
log.debug('Received mode: %s, params: %s' % (mode, params))
|
||||||
|
try:
|
||||||
|
if mode == 'play':
|
||||||
|
result = self.process_play(params.get('id'),
|
||||||
|
params.get('dbid'))
|
||||||
|
elif mode == 'companion':
|
||||||
|
result = self.process_companion()
|
||||||
|
except:
|
||||||
|
log.error('Error encountered for mode %s, params %s'
|
||||||
|
% (mode, params))
|
||||||
|
import traceback
|
||||||
|
log.error(traceback.format_exc())
|
||||||
|
# Let default.py know!
|
||||||
|
pickle_me(None)
|
||||||
|
else:
|
||||||
|
pickle_me(result)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
queue = self.mgr.monitor_kodi_play.playback_queue
|
||||||
|
log.info("----===## Starting Playback_Starter ##===----")
|
||||||
|
while True:
|
||||||
|
item = queue.get()
|
||||||
|
if item is None:
|
||||||
|
# Need to shutdown - initiated by monitor_kodi_play
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
self.triage(item)
|
||||||
|
queue.task_done()
|
||||||
|
log.info("----===## Playback_Starter stopped ##===----")
|
|
@ -3,20 +3,25 @@
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import sys
|
|
||||||
from urllib import urlencode
|
from urllib import urlencode
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
import xbmc
|
from xbmc import getCondVisibility, Player
|
||||||
import xbmcgui
|
import xbmcgui
|
||||||
import xbmcplugin
|
|
||||||
|
|
||||||
import playutils as putils
|
import playutils as putils
|
||||||
import playlist
|
|
||||||
from utils import window, settings, tryEncode, tryDecode
|
from utils import window, settings, tryEncode, tryDecode
|
||||||
import downloadutils
|
import downloadutils
|
||||||
|
|
||||||
import PlexAPI
|
from PlexAPI import API
|
||||||
import PlexFunctions as PF
|
from PlexFunctions import GetPlexPlaylist, KODITYPE_FROM_PLEXTYPE, \
|
||||||
|
PLEX_TYPE_CLIP, PLEX_TYPE_MOVIE
|
||||||
|
from PKC_listitem import PKC_ListItem as ListItem
|
||||||
|
from playlist_func import add_item_to_kodi_playlist, \
|
||||||
|
get_playlist_details_from_xml, add_listitem_to_Kodi_playlist, \
|
||||||
|
add_listitem_to_playlist, remove_from_Kodi_playlist
|
||||||
|
from pickler import Playback_Successful
|
||||||
|
from plexdb_functions import Get_Plex_DB
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
@ -29,74 +34,72 @@ addonName = "PlexKodiConnect"
|
||||||
|
|
||||||
class PlaybackUtils():
|
class PlaybackUtils():
|
||||||
|
|
||||||
def __init__(self, item):
|
def __init__(self, xml, playqueue):
|
||||||
|
self.xml = xml
|
||||||
|
self.playqueue = playqueue
|
||||||
|
|
||||||
self.item = item
|
def play(self, plex_id, kodi_id=None, plex_lib_UUID=None):
|
||||||
self.API = PlexAPI.API(item)
|
"""
|
||||||
|
plex_lib_UUID: xml attribute 'librarySectionUUID', needed for posting
|
||||||
self.userid = window('currUserId')
|
to the PMS
|
||||||
self.server = window('pms_server')
|
"""
|
||||||
|
log.info("Playbackutils called")
|
||||||
if self.API.getType() == 'track':
|
item = self.xml[0]
|
||||||
self.pl = playlist.Playlist(typus='music')
|
api = API(item)
|
||||||
else:
|
playqueue = self.playqueue
|
||||||
self.pl = playlist.Playlist(typus='video')
|
xml = None
|
||||||
|
result = Playback_Successful()
|
||||||
def play(self, itemid, dbid=None):
|
listitem = ListItem()
|
||||||
|
playutils = putils.PlayUtils(item)
|
||||||
item = self.item
|
|
||||||
# Hack to get only existing entry in PMS response for THIS instance of
|
|
||||||
# playbackutils :-)
|
|
||||||
self.API = PlexAPI.API(item[0])
|
|
||||||
API = self.API
|
|
||||||
listitem = xbmcgui.ListItem()
|
|
||||||
playutils = putils.PlayUtils(item[0])
|
|
||||||
|
|
||||||
log.info("Play called.")
|
|
||||||
playurl = playutils.getPlayUrl()
|
playurl = playutils.getPlayUrl()
|
||||||
if not playurl:
|
if not playurl:
|
||||||
return xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, listitem)
|
log.error('No playurl found, aborting')
|
||||||
|
return
|
||||||
|
|
||||||
if dbid in (None, 'plextrailer', 'plexnode'):
|
if kodi_id in (None, 'plextrailer', 'plexnode'):
|
||||||
# Item is not in Kodi database, is a trailer or plex redirect
|
# Item is not in Kodi database, is a trailer/clip or plex redirect
|
||||||
# e.g. plex.tv watch later
|
# e.g. plex.tv watch later
|
||||||
API.CreateListItemFromPlexItem(listitem)
|
api.CreateListItemFromPlexItem(listitem)
|
||||||
self.setArtwork(listitem)
|
api.set_listitem_artwork(listitem)
|
||||||
if dbid == 'plexnode':
|
if kodi_id == 'plexnode':
|
||||||
# Need to get yet another xml to get final url
|
# Need to get yet another xml to get final url
|
||||||
window('emby_%s.playmethod' % playurl, clear=True)
|
window('plex_%s.playmethod' % playurl, clear=True)
|
||||||
xml = downloadutils.DownloadUtils().downloadUrl(
|
xml = downloadutils.DownloadUtils().downloadUrl(
|
||||||
'{server}%s' % item[0][0][0].attrib.get('key'))
|
'{server}%s' % item[0][0].attrib.get('key'))
|
||||||
if xml in (None, 401):
|
try:
|
||||||
|
xml[0].attrib
|
||||||
|
except (TypeError, AttributeError):
|
||||||
log.error('Could not download %s'
|
log.error('Could not download %s'
|
||||||
% item[0][0][0].attrib.get('key'))
|
% item[0][0].attrib.get('key'))
|
||||||
return xbmcplugin.setResolvedUrl(
|
return
|
||||||
int(sys.argv[1]), False, listitem)
|
|
||||||
playurl = tryEncode(xml[0].attrib.get('key'))
|
playurl = tryEncode(xml[0].attrib.get('key'))
|
||||||
window('emby_%s.playmethod' % playurl, value='DirectStream')
|
window('plex_%s.playmethod' % playurl, value='DirectStream')
|
||||||
|
|
||||||
playmethod = window('emby_%s.playmethod' % playurl)
|
playmethod = window('plex_%s.playmethod' % playurl)
|
||||||
if playmethod == "Transcode":
|
if playmethod == "Transcode":
|
||||||
window('emby_%s.playmethod' % playurl, clear=True)
|
window('plex_%s.playmethod' % playurl, clear=True)
|
||||||
playurl = tryEncode(playutils.audioSubsPref(
|
playurl = tryEncode(playutils.audioSubsPref(
|
||||||
listitem, tryDecode(playurl)))
|
listitem, tryDecode(playurl)))
|
||||||
window('emby_%s.playmethod' % playurl, "Transcode")
|
window('plex_%s.playmethod' % playurl, "Transcode")
|
||||||
listitem.setPath(playurl)
|
listitem.setPath(playurl)
|
||||||
self.setProperties(playurl, listitem)
|
api.set_playback_win_props(playurl, listitem)
|
||||||
return xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem)
|
result.listitem = listitem
|
||||||
|
return result
|
||||||
|
|
||||||
############### ORGANIZE CURRENT PLAYLIST ################
|
kodi_type = KODITYPE_FROM_PLEXTYPE[api.getType()]
|
||||||
|
kodi_id = int(kodi_id)
|
||||||
|
|
||||||
|
# ORGANIZE CURRENT PLAYLIST ################
|
||||||
contextmenu_play = window('plex_contextplay') == 'true'
|
contextmenu_play = window('plex_contextplay') == 'true'
|
||||||
window('plex_contextplay', clear=True)
|
window('plex_contextplay', clear=True)
|
||||||
homeScreen = xbmc.getCondVisibility('Window.IsActive(home)')
|
homeScreen = getCondVisibility('Window.IsActive(home)')
|
||||||
kodiPl = self.pl.playlist
|
sizePlaylist = len(playqueue.items)
|
||||||
sizePlaylist = kodiPl.size()
|
|
||||||
if contextmenu_play:
|
if contextmenu_play:
|
||||||
# Need to start with the items we're inserting here
|
# Need to start with the items we're inserting here
|
||||||
startPos = sizePlaylist
|
startPos = sizePlaylist
|
||||||
else:
|
else:
|
||||||
# Can return -1
|
# Can return -1
|
||||||
startPos = max(kodiPl.getposition(), 0)
|
startPos = max(playqueue.kodi_pl.getposition(), 0)
|
||||||
self.currentPosition = startPos
|
self.currentPosition = startPos
|
||||||
|
|
||||||
propertiesPlayback = window('plex_playbackProps') == "true"
|
propertiesPlayback = window('plex_playbackProps') == "true"
|
||||||
|
@ -108,72 +111,106 @@ class PlaybackUtils():
|
||||||
log.info("Playlist plugin position: %s" % self.currentPosition)
|
log.info("Playlist plugin position: %s" % self.currentPosition)
|
||||||
log.info("Playlist size: %s" % sizePlaylist)
|
log.info("Playlist size: %s" % sizePlaylist)
|
||||||
|
|
||||||
############### RESUME POINT ################
|
# RESUME POINT ################
|
||||||
seektime, runtime = API.getRuntime()
|
seektime, runtime = api.getRuntime()
|
||||||
|
if window('plex_customplaylist.seektime'):
|
||||||
|
# Already got seektime, e.g. from playqueue & Plex companion
|
||||||
|
seektime = int(window('plex_customplaylist.seektime'))
|
||||||
|
|
||||||
# We need to ensure we add the intro and additional parts only once.
|
# We need to ensure we add the intro and additional parts only once.
|
||||||
# Otherwise we get a loop.
|
# Otherwise we get a loop.
|
||||||
if not propertiesPlayback:
|
if not propertiesPlayback:
|
||||||
|
|
||||||
window('plex_playbackProps', value="true")
|
window('plex_playbackProps', value="true")
|
||||||
log.info("Setting up properties in playlist.")
|
log.info("Setting up properties in playlist.")
|
||||||
|
# Where will the player need to start?
|
||||||
|
# Do we need to get trailers?
|
||||||
|
trailers = False
|
||||||
|
if (api.getType() == PLEX_TYPE_MOVIE and
|
||||||
|
not seektime and
|
||||||
|
sizePlaylist < 2 and
|
||||||
|
settings('enableCinema') == "true"):
|
||||||
|
if settings('askCinema') == "true":
|
||||||
|
trailers = xbmcgui.Dialog().yesno(
|
||||||
|
addonName,
|
||||||
|
"Play trailers?")
|
||||||
|
else:
|
||||||
|
trailers = True
|
||||||
|
# Post to the PMS. REUSE THE PLAYQUEUE!
|
||||||
|
xml = GetPlexPlaylist(
|
||||||
|
plex_id,
|
||||||
|
plex_lib_UUID,
|
||||||
|
mediatype=api.getType(),
|
||||||
|
trailers=trailers)
|
||||||
|
get_playlist_details_from_xml(playqueue, xml=xml)
|
||||||
|
|
||||||
if (not homeScreen and not seektime and
|
if (not homeScreen and not seektime and sizePlaylist < 2 and
|
||||||
window('plex_customplaylist') != "true" and
|
window('plex_customplaylist') != "true" and
|
||||||
not contextmenu_play):
|
not contextmenu_play):
|
||||||
|
# Need to add a dummy file because the first item will fail
|
||||||
log.debug("Adding dummy file to playlist.")
|
log.debug("Adding dummy file to playlist.")
|
||||||
dummyPlaylist = True
|
dummyPlaylist = True
|
||||||
kodiPl.add(playurl, listitem, index=startPos)
|
add_listitem_to_Kodi_playlist(
|
||||||
|
playqueue,
|
||||||
|
startPos,
|
||||||
|
xbmcgui.ListItem(),
|
||||||
|
playurl,
|
||||||
|
xml[0])
|
||||||
# Remove the original item from playlist
|
# Remove the original item from playlist
|
||||||
self.pl.removefromPlaylist(startPos+1)
|
remove_from_Kodi_playlist(
|
||||||
# Readd the original item to playlist - via jsonrpc so we have full metadata
|
playqueue,
|
||||||
self.pl.insertintoPlaylist(
|
startPos+1)
|
||||||
|
# Readd the original item to playlist - via jsonrpc so we have
|
||||||
|
# full metadata
|
||||||
|
add_item_to_kodi_playlist(
|
||||||
|
playqueue,
|
||||||
self.currentPosition+1,
|
self.currentPosition+1,
|
||||||
dbid,
|
kodi_id=kodi_id,
|
||||||
PF.KODITYPE_FROM_PLEXTYPE[API.getType()])
|
kodi_type=kodi_type,
|
||||||
|
file=playurl)
|
||||||
self.currentPosition += 1
|
self.currentPosition += 1
|
||||||
|
|
||||||
############### -- CHECK FOR INTROS ################
|
# -- ADD TRAILERS ################
|
||||||
if (settings('enableCinema') == "true" and not seektime):
|
if trailers:
|
||||||
# if we have any play them when the movie/show is not being resumed
|
for i, item in enumerate(xml):
|
||||||
xml = PF.GetPlexPlaylist(
|
if i == len(xml) - 1:
|
||||||
itemid,
|
# Don't add the main movie itself
|
||||||
item.attrib.get('librarySectionUUID'),
|
break
|
||||||
mediatype=API.getType())
|
self.add_trailer(item)
|
||||||
introsPlaylist = self.AddTrailers(xml)
|
introsPlaylist = True
|
||||||
|
|
||||||
############### -- ADD MAIN ITEM ONLY FOR HOMESCREEN ##############
|
|
||||||
|
|
||||||
|
# -- ADD MAIN ITEM ONLY FOR HOMESCREEN ##############
|
||||||
if homeScreen and not seektime and not sizePlaylist:
|
if homeScreen and not seektime and not sizePlaylist:
|
||||||
# Extend our current playlist with the actual item to play
|
# Extend our current playlist with the actual item to play
|
||||||
# only if there's no playlist first
|
# only if there's no playlist first
|
||||||
log.info("Adding main item to playlist.")
|
log.info("Adding main item to playlist.")
|
||||||
self.pl.addtoPlaylist(
|
add_item_to_kodi_playlist(
|
||||||
dbid,
|
playqueue,
|
||||||
PF.KODITYPE_FROM_PLEXTYPE[API.getType()])
|
self.currentPosition,
|
||||||
|
kodi_id,
|
||||||
|
kodi_type)
|
||||||
|
|
||||||
elif contextmenu_play:
|
elif contextmenu_play:
|
||||||
if window('useDirectPaths') == 'true':
|
if window('useDirectPaths') == 'true':
|
||||||
# Cannot add via JSON with full metadata because then we
|
# Cannot add via JSON with full metadata because then we
|
||||||
# Would be using the direct path
|
# Would be using the direct path
|
||||||
log.debug("Adding contextmenu item for direct paths")
|
log.debug("Adding contextmenu item for direct paths")
|
||||||
if window('emby_%s.playmethod' % playurl) == "Transcode":
|
if window('plex_%s.playmethod' % playurl) == "Transcode":
|
||||||
window('emby_%s.playmethod' % playurl,
|
window('plex_%s.playmethod' % playurl,
|
||||||
clear=True)
|
clear=True)
|
||||||
playurl = tryEncode(playutils.audioSubsPref(
|
playurl = tryEncode(playutils.audioSubsPref(
|
||||||
listitem, tryDecode(playurl)))
|
listitem, tryDecode(playurl)))
|
||||||
window('emby_%s.playmethod' % playurl,
|
window('plex_%s.playmethod' % playurl,
|
||||||
value="Transcode")
|
value="Transcode")
|
||||||
self.setProperties(playurl, listitem)
|
api.CreateListItemFromPlexItem(listitem)
|
||||||
self.setArtwork(listitem)
|
api.set_playback_win_props(playurl, listitem)
|
||||||
API.CreateListItemFromPlexItem(listitem)
|
api.set_listitem_artwork(listitem)
|
||||||
kodiPl.add(playurl, listitem, index=self.currentPosition+1)
|
kodiPl.add(playurl, listitem, index=self.currentPosition+1)
|
||||||
else:
|
else:
|
||||||
# Full metadata
|
# Full metadata
|
||||||
self.pl.insertintoPlaylist(
|
self.pl.insertintoPlaylist(
|
||||||
self.currentPosition+1,
|
self.currentPosition+1,
|
||||||
dbid,
|
kodi_id,
|
||||||
PF.KODITYPE_FROM_PLEXTYPE[API.getType()])
|
kodi_type)
|
||||||
self.currentPosition += 1
|
self.currentPosition += 1
|
||||||
if seektime:
|
if seektime:
|
||||||
window('plex_customplaylist.seektime', value=str(seektime))
|
window('plex_customplaylist.seektime', value=str(seektime))
|
||||||
|
@ -181,177 +218,145 @@ class PlaybackUtils():
|
||||||
# Ensure that additional parts are played after the main item
|
# Ensure that additional parts are played after the main item
|
||||||
self.currentPosition += 1
|
self.currentPosition += 1
|
||||||
|
|
||||||
############### -- CHECK FOR ADDITIONAL PARTS ################
|
# -- CHECK FOR ADDITIONAL PARTS ################
|
||||||
if len(item[0][0]) > 1:
|
if len(item[0]) > 1:
|
||||||
# Only add to the playlist after intros have played
|
self.add_part(item, api, kodi_id, kodi_type)
|
||||||
for counter, part in enumerate(item[0][0]):
|
|
||||||
# Never add first part
|
|
||||||
if counter == 0:
|
|
||||||
continue
|
|
||||||
# Set listitem and properties for each additional parts
|
|
||||||
API.setPartNumber(counter)
|
|
||||||
additionalListItem = xbmcgui.ListItem()
|
|
||||||
additionalPlayurl = playutils.getPlayUrl(
|
|
||||||
partNumber=counter)
|
|
||||||
log.debug("Adding additional part: %s, url: %s"
|
|
||||||
% (counter, additionalPlayurl))
|
|
||||||
|
|
||||||
self.setProperties(additionalPlayurl, additionalListItem)
|
|
||||||
self.setArtwork(additionalListItem)
|
|
||||||
# NEW to Plex
|
|
||||||
API.CreateListItemFromPlexItem(additionalListItem)
|
|
||||||
|
|
||||||
kodiPl.add(additionalPlayurl, additionalListItem,
|
|
||||||
index=self.currentPosition)
|
|
||||||
self.pl.verifyPlaylist()
|
|
||||||
self.currentPosition += 1
|
|
||||||
API.setPartNumber(0)
|
|
||||||
|
|
||||||
if dummyPlaylist:
|
if dummyPlaylist:
|
||||||
# Added a dummy file to the playlist,
|
# Added a dummy file to the playlist,
|
||||||
# because the first item is going to fail automatically.
|
# because the first item is going to fail automatically.
|
||||||
log.info("Processed as a playlist. First item is skipped.")
|
log.info("Processed as a playlist. First item is skipped.")
|
||||||
return xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, listitem)
|
# Delete the item that's gonna fail!
|
||||||
|
del playqueue.items[startPos]
|
||||||
|
# Don't attach listitem
|
||||||
|
return result
|
||||||
|
|
||||||
# We just skipped adding properties. Reset flag for next time.
|
# We just skipped adding properties. Reset flag for next time.
|
||||||
elif propertiesPlayback:
|
elif propertiesPlayback:
|
||||||
log.debug("Resetting properties playback flag.")
|
log.debug("Resetting properties playback flag.")
|
||||||
window('plex_playbackProps', clear=True)
|
window('plex_playbackProps', clear=True)
|
||||||
|
|
||||||
#self.pl.verifyPlaylist()
|
# SETUP MAIN ITEM ##########
|
||||||
########## SETUP MAIN ITEM ##########
|
|
||||||
# For transcoding only, ask for audio/subs pref
|
# For transcoding only, ask for audio/subs pref
|
||||||
if (window('emby_%s.playmethod' % playurl) == "Transcode" and
|
if (window('plex_%s.playmethod' % playurl) == "Transcode" and
|
||||||
not contextmenu_play):
|
not contextmenu_play):
|
||||||
window('emby_%s.playmethod' % playurl, clear=True)
|
window('plex_%s.playmethod' % playurl, clear=True)
|
||||||
playurl = tryEncode(playutils.audioSubsPref(
|
playurl = tryEncode(playutils.audioSubsPref(
|
||||||
listitem, tryDecode(playurl)))
|
listitem, tryDecode(playurl)))
|
||||||
window('emby_%s.playmethod' % playurl, value="Transcode")
|
window('plex_%s.playmethod' % playurl, value="Transcode")
|
||||||
|
|
||||||
listitem.setPath(playurl)
|
listitem.setPath(playurl)
|
||||||
self.setProperties(playurl, listitem)
|
api.set_playback_win_props(playurl, listitem)
|
||||||
|
api.set_listitem_artwork(listitem)
|
||||||
|
|
||||||
############### PLAYBACK ################
|
# PLAYBACK ################
|
||||||
if (homeScreen and seektime and window('plex_customplaylist') != "true"
|
if (homeScreen and seektime and window('plex_customplaylist') != "true"
|
||||||
and not contextmenu_play):
|
and not contextmenu_play):
|
||||||
log.info("Play as a widget item.")
|
log.info("Play as a widget item")
|
||||||
API.CreateListItemFromPlexItem(listitem)
|
api.CreateListItemFromPlexItem(listitem)
|
||||||
xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem)
|
result.listitem = listitem
|
||||||
|
return result
|
||||||
|
|
||||||
elif ((introsPlaylist and window('plex_customplaylist') == "true") or
|
elif ((introsPlaylist and window('plex_customplaylist') == "true") or
|
||||||
(homeScreen and not sizePlaylist) or
|
(homeScreen and not sizePlaylist) or
|
||||||
contextmenu_play):
|
contextmenu_play):
|
||||||
# Playlist was created just now, play it.
|
# Playlist was created just now, play it.
|
||||||
# Contextmenu plays always need this
|
# Contextmenu plays always need this
|
||||||
log.info("Play playlist.")
|
log.info("Play playlist from starting position %s" % startPos)
|
||||||
xbmcplugin.endOfDirectory(int(sys.argv[1]), True, False, False)
|
# Need a separate thread because Player won't return in time
|
||||||
xbmc.Player().play(kodiPl, startpos=startPos)
|
thread = Thread(target=Player().play,
|
||||||
|
args=(playqueue.kodi_pl, None, False, startPos))
|
||||||
|
thread.setDaemon(True)
|
||||||
|
thread.start()
|
||||||
|
# Don't attach listitem
|
||||||
|
return result
|
||||||
else:
|
else:
|
||||||
log.info("Play as a regular item.")
|
log.info("Play as a regular item")
|
||||||
xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem)
|
result.listitem = listitem
|
||||||
|
return result
|
||||||
|
|
||||||
def AddTrailers(self, xml):
|
def play_all(self):
|
||||||
"""
|
"""
|
||||||
Adds trailers to a movie, if applicable. Returns True if trailers were
|
Play all items contained in the xml passed in. Called by Plex Companion
|
||||||
added
|
|
||||||
"""
|
"""
|
||||||
# Failure when downloading trailer playQueue
|
log.info("Playbackutils play_all called")
|
||||||
if xml in (None, 401):
|
window('plex_playbackProps', value="true")
|
||||||
return False
|
self.currentPosition = 0
|
||||||
# Failure when getting trailers, e.g. when no plex pass
|
for item in self.xml:
|
||||||
if xml.attrib.get('size') == '1':
|
api = API(item)
|
||||||
return False
|
if api.getType() == PLEX_TYPE_CLIP:
|
||||||
|
self.add_trailer(item)
|
||||||
if settings('askCinema') == "true":
|
continue
|
||||||
resp = xbmcgui.Dialog().yesno(addonName, "Play trailers?")
|
with Get_Plex_DB() as plex_db:
|
||||||
if not resp:
|
db_item = plex_db.getItem_byId(api.getRatingKey())
|
||||||
# User selected to not play trailers
|
try:
|
||||||
log.info("Skip trailers.")
|
add_item_to_kodi_playlist(self.playqueue,
|
||||||
return False
|
self.currentPosition,
|
||||||
|
kodi_id=db_item[0],
|
||||||
|
kodi_type=db_item[4])
|
||||||
|
self.currentPosition += 1
|
||||||
|
if len(item[0]) > 1:
|
||||||
|
self.add_part(item,
|
||||||
|
api,
|
||||||
|
db_item[0],
|
||||||
|
db_item[4])
|
||||||
|
except TypeError:
|
||||||
|
# Item not in Kodi DB
|
||||||
|
self.add_trailer(item)
|
||||||
|
continue
|
||||||
|
|
||||||
|
def add_trailer(self, item):
|
||||||
# Playurl needs to point back so we can get metadata!
|
# Playurl needs to point back so we can get metadata!
|
||||||
path = "plugin://plugin.video.plexkodiconnect.movies/"
|
path = "plugin://plugin.video.plexkodiconnect/movies/"
|
||||||
params = {
|
params = {
|
||||||
'mode': "play",
|
'mode': "play",
|
||||||
'dbid': 'plextrailer'
|
'dbid': 'plextrailer'
|
||||||
}
|
}
|
||||||
for counter, intro in enumerate(xml):
|
introAPI = API(item)
|
||||||
# Don't process the last item - it's the original movie
|
listitem = introAPI.CreateListItemFromPlexItem()
|
||||||
if counter == len(xml)-1:
|
params['id'] = introAPI.getRatingKey()
|
||||||
break
|
params['filename'] = introAPI.getKey()
|
||||||
# The server randomly returns intros, process them.
|
introPlayurl = path + '?' + urlencode(params)
|
||||||
# introListItem = xbmcgui.ListItem()
|
introAPI.set_listitem_artwork(listitem)
|
||||||
# introPlayurl = putils.PlayUtils(intro).getPlayUrl()
|
# Overwrite the Plex url
|
||||||
introAPI = PlexAPI.API(intro)
|
listitem.setPath(introPlayurl)
|
||||||
params['id'] = introAPI.getRatingKey()
|
log.info("Adding Plex trailer: %s" % introPlayurl)
|
||||||
params['filename'] = introAPI.getKey()
|
add_listitem_to_Kodi_playlist(
|
||||||
introPlayurl = path + '?' + urlencode(params)
|
self.playqueue,
|
||||||
log.info("Adding Intro: %s" % introPlayurl)
|
self.currentPosition,
|
||||||
|
listitem,
|
||||||
|
introPlayurl,
|
||||||
|
xml_video_element=item)
|
||||||
|
self.currentPosition += 1
|
||||||
|
|
||||||
self.pl.insertintoPlaylist(self.currentPosition, url=introPlayurl)
|
def add_part(self, item, api, kodi_id, kodi_type):
|
||||||
|
"""
|
||||||
|
Adds an additional part to the playlist
|
||||||
|
"""
|
||||||
|
# Only add to the playlist after intros have played
|
||||||
|
for counter, part in enumerate(item[0]):
|
||||||
|
# Never add first part
|
||||||
|
if counter == 0:
|
||||||
|
continue
|
||||||
|
# Set listitem and properties for each additional parts
|
||||||
|
api.setPartNumber(counter)
|
||||||
|
additionalListItem = xbmcgui.ListItem()
|
||||||
|
playutils = putils.PlayUtils(item)
|
||||||
|
additionalPlayurl = playutils.getPlayUrl(
|
||||||
|
partNumber=counter)
|
||||||
|
log.debug("Adding additional part: %s, url: %s"
|
||||||
|
% (counter, additionalPlayurl))
|
||||||
|
api.CreateListItemFromPlexItem(additionalListItem)
|
||||||
|
api.set_playback_win_props(additionalPlayurl,
|
||||||
|
additionalListItem)
|
||||||
|
api.set_listitem_artwork(additionalListItem)
|
||||||
|
add_listitem_to_playlist(
|
||||||
|
self.playqueue,
|
||||||
|
self.currentPosition,
|
||||||
|
additionalListItem,
|
||||||
|
kodi_id=kodi_id,
|
||||||
|
kodi_type=kodi_type,
|
||||||
|
plex_id=api.getRatingKey(),
|
||||||
|
file=additionalPlayurl)
|
||||||
self.currentPosition += 1
|
self.currentPosition += 1
|
||||||
|
api.setPartNumber(0)
|
||||||
return True
|
|
||||||
|
|
||||||
def setProperties(self, playurl, listitem):
|
|
||||||
# Set all properties necessary for plugin path playback
|
|
||||||
itemid = self.API.getRatingKey()
|
|
||||||
itemtype = self.API.getType()
|
|
||||||
userdata = self.API.getUserData()
|
|
||||||
|
|
||||||
embyitem = "emby_%s" % playurl
|
|
||||||
window('%s.runtime' % embyitem, value=str(userdata['Runtime']))
|
|
||||||
window('%s.type' % embyitem, value=itemtype)
|
|
||||||
window('%s.itemid' % embyitem, value=itemid)
|
|
||||||
window('%s.playcount' % embyitem, value=str(userdata['PlayCount']))
|
|
||||||
|
|
||||||
if itemtype == "episode":
|
|
||||||
window('%s.refreshid' % embyitem,
|
|
||||||
value=self.API.getParentRatingKey())
|
|
||||||
else:
|
|
||||||
window('%s.refreshid' % embyitem, value=itemid)
|
|
||||||
|
|
||||||
# Append external subtitles to stream
|
|
||||||
playmethod = window('%s.playmethod' % embyitem)
|
|
||||||
if playmethod in ("DirectStream", "DirectPlay"):
|
|
||||||
subtitles = self.API.externalSubs(playurl)
|
|
||||||
listitem.setSubtitles(subtitles)
|
|
||||||
|
|
||||||
self.setArtwork(listitem)
|
|
||||||
|
|
||||||
def setArtwork(self, listItem):
|
|
||||||
allartwork = self.API.getAllArtwork(parentInfo=True)
|
|
||||||
arttypes = {
|
|
||||||
'poster': "Primary",
|
|
||||||
'tvshow.poster': "Thumb",
|
|
||||||
'clearart': "Art",
|
|
||||||
'tvshow.clearart': "Art",
|
|
||||||
'clearart': "Primary",
|
|
||||||
'tvshow.clearart': "Primary",
|
|
||||||
'clearlogo': "Logo",
|
|
||||||
'tvshow.clearlogo': "Logo",
|
|
||||||
'discart': "Disc",
|
|
||||||
'fanart_image': "Backdrop",
|
|
||||||
'landscape': "Backdrop",
|
|
||||||
"banner": "Banner"
|
|
||||||
}
|
|
||||||
for arttype in arttypes:
|
|
||||||
art = arttypes[arttype]
|
|
||||||
if art == "Backdrop":
|
|
||||||
try:
|
|
||||||
# Backdrop is a list, grab the first backdrop
|
|
||||||
self.setArtProp(listItem, arttype, allartwork[art][0])
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
self.setArtProp(listItem, arttype, allartwork[art])
|
|
||||||
|
|
||||||
def setArtProp(self, listItem, arttype, path):
|
|
||||||
if arttype in (
|
|
||||||
'thumb', 'fanart_image', 'small_poster', 'tiny_poster',
|
|
||||||
'medium_landscape', 'medium_poster', 'small_fanartimage',
|
|
||||||
'medium_fanartimage', 'fanart_noindicators'):
|
|
||||||
listItem.setProperty(arttype, path)
|
|
||||||
else:
|
|
||||||
listItem.setArt({arttype: path})
|
|
||||||
|
|
|
@ -11,8 +11,9 @@ from utils import window, settings, language as lang, DateToKodi, \
|
||||||
getUnixTimestamp
|
getUnixTimestamp
|
||||||
import clientinfo
|
import clientinfo
|
||||||
import downloadutils
|
import downloadutils
|
||||||
import embydb_functions as embydb
|
import plexdb_functions as plexdb
|
||||||
import kodidb_functions as kodidb
|
import kodidb_functions as kodidb
|
||||||
|
from PlexFunctions import KODI_TYPE_MOVIE, KODI_TYPE_EPISODE
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
@ -76,11 +77,11 @@ class Player(xbmc.Player):
|
||||||
self.currentFile = currentFile
|
self.currentFile = currentFile
|
||||||
window('plex_lastPlayedFiled', value=currentFile)
|
window('plex_lastPlayedFiled', value=currentFile)
|
||||||
# We may need to wait for info to be set in kodi monitor
|
# We may need to wait for info to be set in kodi monitor
|
||||||
itemId = window("emby_%s.itemid" % currentFile)
|
itemId = window("plex_%s.itemid" % currentFile)
|
||||||
count = 0
|
count = 0
|
||||||
while not itemId:
|
while not itemId:
|
||||||
xbmc.sleep(200)
|
xbmc.sleep(200)
|
||||||
itemId = window("emby_%s.itemid" % currentFile)
|
itemId = window("plex_%s.itemid" % currentFile)
|
||||||
if count == 5:
|
if count == 5:
|
||||||
log.warn("Could not find itemId, cancelling playback report!")
|
log.warn("Could not find itemId, cancelling playback report!")
|
||||||
return
|
return
|
||||||
|
@ -88,16 +89,16 @@ class Player(xbmc.Player):
|
||||||
|
|
||||||
log.info("ONPLAYBACK_STARTED: %s itemid: %s" % (currentFile, itemId))
|
log.info("ONPLAYBACK_STARTED: %s itemid: %s" % (currentFile, itemId))
|
||||||
|
|
||||||
embyitem = "emby_%s" % currentFile
|
plexitem = "plex_%s" % currentFile
|
||||||
runtime = window("%s.runtime" % embyitem)
|
runtime = window("%s.runtime" % plexitem)
|
||||||
refresh_id = window("%s.refreshid" % embyitem)
|
refresh_id = window("%s.refreshid" % plexitem)
|
||||||
playMethod = window("%s.playmethod" % embyitem)
|
playMethod = window("%s.playmethod" % plexitem)
|
||||||
itemType = window("%s.type" % embyitem)
|
itemType = window("%s.type" % plexitem)
|
||||||
try:
|
try:
|
||||||
playcount = int(window("%s.playcount" % embyitem))
|
playcount = int(window("%s.playcount" % plexitem))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
playcount = 0
|
playcount = 0
|
||||||
window('emby_skipWatched%s' % itemId, value="true")
|
window('plex_skipWatched%s' % itemId, value="true")
|
||||||
|
|
||||||
log.debug("Playing itemtype is: %s" % itemType)
|
log.debug("Playing itemtype is: %s" % itemType)
|
||||||
|
|
||||||
|
@ -134,7 +135,7 @@ class Player(xbmc.Player):
|
||||||
volume = result.get('volume')
|
volume = result.get('volume')
|
||||||
muted = result.get('muted')
|
muted = result.get('muted')
|
||||||
|
|
||||||
# Postdata structure to send to Emby server
|
# Postdata structure to send to plex server
|
||||||
url = "{server}/:/timeline?"
|
url = "{server}/:/timeline?"
|
||||||
postdata = {
|
postdata = {
|
||||||
|
|
||||||
|
@ -154,7 +155,7 @@ class Player(xbmc.Player):
|
||||||
postdata['AudioStreamIndex'] = window("%sAudioStreamIndex" % currentFile)
|
postdata['AudioStreamIndex'] = window("%sAudioStreamIndex" % currentFile)
|
||||||
postdata['SubtitleStreamIndex'] = window("%sSubtitleStreamIndex" % currentFile)
|
postdata['SubtitleStreamIndex'] = window("%sSubtitleStreamIndex" % currentFile)
|
||||||
else:
|
else:
|
||||||
# Get the current kodi audio and subtitles and convert to Emby equivalent
|
# Get the current kodi audio and subtitles and convert to plex equivalent
|
||||||
tracks_query = {
|
tracks_query = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
@ -190,9 +191,9 @@ class Player(xbmc.Player):
|
||||||
# Postdata for the subtitles
|
# Postdata for the subtitles
|
||||||
if subsEnabled and len(xbmc.Player().getAvailableSubtitleStreams()) > 0:
|
if subsEnabled and len(xbmc.Player().getAvailableSubtitleStreams()) > 0:
|
||||||
|
|
||||||
# Number of audiotracks to help get Emby Index
|
# Number of audiotracks to help get plex Index
|
||||||
audioTracks = len(xbmc.Player().getAvailableAudioStreams())
|
audioTracks = len(xbmc.Player().getAvailableAudioStreams())
|
||||||
mapping = window("%s.indexMapping" % embyitem)
|
mapping = window("%s.indexMapping" % plexitem)
|
||||||
|
|
||||||
if mapping: # Set in playbackutils.py
|
if mapping: # Set in playbackutils.py
|
||||||
|
|
||||||
|
@ -229,10 +230,10 @@ class Player(xbmc.Player):
|
||||||
log.error('Could not get kodi runtime, setting to zero')
|
log.error('Could not get kodi runtime, setting to zero')
|
||||||
runtime = 0
|
runtime = 0
|
||||||
|
|
||||||
with embydb.GetEmbyDB() as emby_db:
|
with plexdb.Get_Plex_DB() as plex_db:
|
||||||
emby_dbitem = emby_db.getItem_byId(itemId)
|
plex_dbitem = plex_db.getItem_byId(itemId)
|
||||||
try:
|
try:
|
||||||
fileid = emby_dbitem[1]
|
fileid = plex_dbitem[1]
|
||||||
except TypeError:
|
except TypeError:
|
||||||
log.info("Could not find fileid in plex db.")
|
log.info("Could not find fileid in plex db.")
|
||||||
fileid = None
|
fileid = None
|
||||||
|
@ -338,7 +339,7 @@ class Player(xbmc.Player):
|
||||||
playMethod = data['playmethod']
|
playMethod = data['playmethod']
|
||||||
|
|
||||||
# Prevent manually mark as watched in Kodi monitor
|
# Prevent manually mark as watched in Kodi monitor
|
||||||
window('emby_skipWatched%s' % itemid, value="true")
|
window('plex_skipWatched%s' % itemid, value="true")
|
||||||
|
|
||||||
if currentPosition and runtime:
|
if currentPosition and runtime:
|
||||||
try:
|
try:
|
||||||
|
@ -353,7 +354,7 @@ class Player(xbmc.Player):
|
||||||
if percentComplete >= markPlayed:
|
if percentComplete >= markPlayed:
|
||||||
# Tell Kodi that we've finished watching (Plex knows)
|
# Tell Kodi that we've finished watching (Plex knows)
|
||||||
if (data['fileid'] is not None and
|
if (data['fileid'] is not None and
|
||||||
data['itemType'] in ('movie', 'episode')):
|
data['itemType'] in (KODI_TYPE_MOVIE, KODI_TYPE_EPISODE)):
|
||||||
with kodidb.GetKodiDB('video') as kodi_db:
|
with kodidb.GetKodiDB('video') as kodi_db:
|
||||||
kodi_db.addPlaystate(
|
kodi_db.addPlaystate(
|
||||||
data['fileid'],
|
data['fileid'],
|
||||||
|
@ -391,13 +392,13 @@ class Player(xbmc.Player):
|
||||||
# Clean the WINDOW properties
|
# Clean the WINDOW properties
|
||||||
for filename in self.played_info:
|
for filename in self.played_info:
|
||||||
cleanup = (
|
cleanup = (
|
||||||
'emby_%s.itemid' % filename,
|
'plex_%s.itemid' % filename,
|
||||||
'emby_%s.runtime' % filename,
|
'plex_%s.runtime' % filename,
|
||||||
'emby_%s.refreshid' % filename,
|
'plex_%s.refreshid' % filename,
|
||||||
'emby_%s.playmethod' % filename,
|
'plex_%s.playmethod' % filename,
|
||||||
'emby_%s.type' % filename,
|
'plex_%s.type' % filename,
|
||||||
'emby_%s.runtime' % filename,
|
'plex_%s.runtime' % filename,
|
||||||
'emby_%s.playcount' % filename,
|
'plex_%s.playcount' % filename,
|
||||||
'plex_%s.playlistPosition' % filename
|
'plex_%s.playlistPosition' % filename
|
||||||
)
|
)
|
||||||
for item in cleanup:
|
for item in cleanup:
|
||||||
|
|
|
@ -1,337 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import json
|
|
||||||
from urllib import urlencode
|
|
||||||
from threading import Lock
|
|
||||||
from functools import wraps
|
|
||||||
|
|
||||||
import xbmc
|
|
||||||
|
|
||||||
import embydb_functions as embydb
|
|
||||||
from utils import window, tryEncode
|
|
||||||
import playbackutils
|
|
||||||
import PlexFunctions
|
|
||||||
import PlexAPI
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
|
|
||||||
log = logging.getLogger("PLEX."+__name__)
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
|
|
||||||
|
|
||||||
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')
|
|
||||||
"""
|
|
||||||
# Borg - multiple instances, shared state
|
|
||||||
_shared_state = {}
|
|
||||||
|
|
||||||
typus = None
|
|
||||||
queueId = None
|
|
||||||
playQueueVersion = None
|
|
||||||
guid = None
|
|
||||||
playlistId = None
|
|
||||||
player = xbmc.Player()
|
|
||||||
# "interal" PKC playlist
|
|
||||||
items = []
|
|
||||||
|
|
||||||
@lockMethod.decorate
|
|
||||||
def __init__(self, typus=None):
|
|
||||||
# Borg
|
|
||||||
self.__dict__ = self._shared_state
|
|
||||||
|
|
||||||
self.userid = window('currUserId')
|
|
||||||
self.server = window('pms_server')
|
|
||||||
# Construct the Kodi playlist instance
|
|
||||||
if self.typus == typus:
|
|
||||||
return
|
|
||||||
if typus == 'video':
|
|
||||||
self.playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
|
|
||||||
self.typus = 'video'
|
|
||||||
log.info('Initiated video playlist')
|
|
||||||
elif typus == 'music':
|
|
||||||
self.playlist = xbmc.PlayList(xbmc.PLAYLIST_MUSIC)
|
|
||||||
self.typus = 'music'
|
|
||||||
log.info('Initiated music playlist')
|
|
||||||
else:
|
|
||||||
self.playlist = None
|
|
||||||
self.typus = None
|
|
||||||
log.info('Empty playlist initiated')
|
|
||||||
if self.playlist is not None:
|
|
||||||
self.playlistId = self.playlist.getPlayListId()
|
|
||||||
|
|
||||||
@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 = PlexFunctions.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 = PlexFunctions.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)))
|
|
519
resources/lib/playlist_func.py
Normal file
519
resources/lib/playlist_func.py
Normal file
|
@ -0,0 +1,519 @@
|
||||||
|
import logging
|
||||||
|
from urllib import quote
|
||||||
|
|
||||||
|
import plexdb_functions as plexdb
|
||||||
|
from downloadutils import DownloadUtils as DU
|
||||||
|
from utils import JSONRPC, tryEncode
|
||||||
|
from PlexAPI import API
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
log = logging.getLogger("PLEX."+__name__)
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
# kodi_item dict:
|
||||||
|
# {u'type': u'movie', u'id': 3, 'file': path-to-file}
|
||||||
|
|
||||||
|
|
||||||
|
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: " % (self.__class__.__name__)
|
||||||
|
# For some reason, can't use dir directly
|
||||||
|
answ += "ID: %s, " % self.ID
|
||||||
|
answ += "items: %s, " % self.items
|
||||||
|
for key in self.__dict__:
|
||||||
|
if key not in ("ID", 'items'):
|
||||||
|
answ += '%s: %s, ' % (key, getattr(self, key))
|
||||||
|
return answ[:-2] + ">"
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
"""
|
||||||
|
Resets the playlist object to an empty playlist
|
||||||
|
"""
|
||||||
|
# Clear Kodi playlist object
|
||||||
|
self.kodi_pl.clear()
|
||||||
|
self.items = []
|
||||||
|
self.old_kodi_pl = []
|
||||||
|
self.ID = None
|
||||||
|
self.version = None
|
||||||
|
self.selectedItemID = None
|
||||||
|
self.selectedItemOffset = None
|
||||||
|
self.shuffled = 0
|
||||||
|
self.repeat = 0
|
||||||
|
log.debug('Playlist cleared: %s' % self)
|
||||||
|
|
||||||
|
def log_Kodi_playlist(self):
|
||||||
|
log.debug('Current Kodi playlist: %s' % get_kodi_playlist_items(self))
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
guid = None # Weird Plex guid
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
answ = "<%s: " % (self.__class__.__name__)
|
||||||
|
for key in self.__dict__:
|
||||||
|
answ += '%s: %s, ' % (key, getattr(self, key))
|
||||||
|
return answ[:-2] + ">"
|
||||||
|
|
||||||
|
|
||||||
|
def playlist_item_from_kodi(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()
|
||||||
|
item.kodi_id = kodi_item.get('id')
|
||||||
|
if item.kodi_id:
|
||||||
|
with plexdb.Get_Plex_DB() as plex_db:
|
||||||
|
plex_dbitem = plex_db.getItem_byKodiId(kodi_item['id'],
|
||||||
|
kodi_item['type'])
|
||||||
|
try:
|
||||||
|
item.plex_id = plex_dbitem[0]
|
||||||
|
item.plex_UUID = plex_dbitem[0] # we dont need the uuid yet :-)
|
||||||
|
except TypeError:
|
||||||
|
pass
|
||||||
|
item.file = kodi_item.get('file')
|
||||||
|
item.kodi_type = kodi_item.get('type')
|
||||||
|
if item.plex_id is None:
|
||||||
|
item.uri = 'library://whatever/item/%s' % quote(item.file, safe='')
|
||||||
|
else:
|
||||||
|
# TO BE VERIFIED - PLEX DOESN'T LIKE PLAYLIST ADDS IN THIS MANNER
|
||||||
|
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")
|
||||||
|
|
||||||
|
Returns a Playlist_Item
|
||||||
|
"""
|
||||||
|
item = Playlist_Item()
|
||||||
|
item.plex_id = plex_id
|
||||||
|
with plexdb.Get_Plex_DB() as plex_db:
|
||||||
|
plex_dbitem = plex_db.getItem_byId(plex_id)
|
||||||
|
try:
|
||||||
|
item.kodi_id = plex_dbitem[0]
|
||||||
|
item.kodi_type = plex_dbitem[4]
|
||||||
|
except:
|
||||||
|
raise KeyError('Could not find plex_id %s in database' % plex_id)
|
||||||
|
return item
|
||||||
|
|
||||||
|
|
||||||
|
def playlist_item_from_xml(playlist, xml_video_element):
|
||||||
|
"""
|
||||||
|
Returns a playlist element for the playqueue using the Plex xml
|
||||||
|
"""
|
||||||
|
item = Playlist_Item()
|
||||||
|
api = API(xml_video_element)
|
||||||
|
item.plex_id = api.getRatingKey()
|
||||||
|
item.ID = xml_video_element.attrib['%sItemID' % playlist.kind]
|
||||||
|
item.guid = xml_video_element.attrib.get('guid')
|
||||||
|
if item.plex_id:
|
||||||
|
with plexdb.Get_Plex_DB() as plex_db:
|
||||||
|
db_element = plex_db.getItem_byId(item.plex_id)
|
||||||
|
try:
|
||||||
|
item.kodi_id, item.kodi_type = int(db_element[0]), db_element[4]
|
||||||
|
except TypeError:
|
||||||
|
pass
|
||||||
|
log.debug('Created new playlist item from xml: %s' % item)
|
||||||
|
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:')
|
||||||
|
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.shuffled = xml.attrib['%sShuffled' % playlist.kind]
|
||||||
|
playlist.selectedItemID = xml.attrib.get(
|
||||||
|
'%sSelectedItemID' % playlist.kind)
|
||||||
|
playlist.selectedItemOffset = xml.attrib.get(
|
||||||
|
'%sSelectedItemOffset' % 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
|
||||||
|
log.debug('Updated playlist from xml: %s' % playlist)
|
||||||
|
|
||||||
|
|
||||||
|
def update_playlist_from_PMS(playlist, playlist_id=None, xml=None):
|
||||||
|
"""
|
||||||
|
Updates Kodi playlist using a new PMS playlist. Pass in playlist_id if we
|
||||||
|
need to fetch a new playqueue
|
||||||
|
|
||||||
|
If an xml is passed in, the playlist will be overwritten with its info
|
||||||
|
"""
|
||||||
|
if xml is None:
|
||||||
|
xml = get_PMS_playlist(playlist, playlist_id)
|
||||||
|
try:
|
||||||
|
xml.attrib['%sVersion' % playlist.kind]
|
||||||
|
except:
|
||||||
|
log.error('Could not process Plex playlist')
|
||||||
|
return
|
||||||
|
# Clear our existing playlist and the associated Kodi playlist
|
||||||
|
playlist.clear()
|
||||||
|
# Set new values
|
||||||
|
get_playlist_details_from_xml(playlist, xml)
|
||||||
|
for plex_item in xml:
|
||||||
|
playlist.items.append(add_to_Kodi_playlist(playlist, plex_item))
|
||||||
|
|
||||||
|
|
||||||
|
def init_Plex_playlist(playlist, plex_id=None, kodi_item=None):
|
||||||
|
"""
|
||||||
|
Initializes the Plex side without changing the Kodi playlists
|
||||||
|
|
||||||
|
WILL ALSO UPDATE OUR PLAYLISTS
|
||||||
|
"""
|
||||||
|
log.debug('Initializing the playlist %s on the Plex side' % playlist)
|
||||||
|
if plex_id:
|
||||||
|
item = playlist_item_from_plex(plex_id)
|
||||||
|
else:
|
||||||
|
item = playlist_item_from_kodi(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(playlist, xml)
|
||||||
|
playlist.items.append(item)
|
||||||
|
log.debug('Initialized the playlist on the Plex side: %s' % playlist)
|
||||||
|
|
||||||
|
|
||||||
|
def add_listitem_to_playlist(playlist, pos, listitem, kodi_id=None,
|
||||||
|
kodi_type=None, plex_id=None, file=None):
|
||||||
|
"""
|
||||||
|
Adds a listitem to both the Kodi and Plex playlist at position pos [int].
|
||||||
|
|
||||||
|
If file is not None, file will overrule kodi_id!
|
||||||
|
"""
|
||||||
|
log.debug('add_listitem_to_playlist. Playlist before add: %s' % playlist)
|
||||||
|
kodi_item = {'id': kodi_id, 'type': kodi_type, 'file': file}
|
||||||
|
if playlist.ID is None:
|
||||||
|
init_Plex_playlist(playlist, plex_id, kodi_item)
|
||||||
|
else:
|
||||||
|
add_item_to_PMS_playlist(playlist, pos, plex_id, kodi_item)
|
||||||
|
if kodi_id is None and playlist.items[pos].kodi_id:
|
||||||
|
kodi_id = playlist.items[pos].kodi_id
|
||||||
|
kodi_type = playlist.items[pos].kodi_type
|
||||||
|
if file is None:
|
||||||
|
file = playlist.items[pos].file
|
||||||
|
# Otherwise we double the item!
|
||||||
|
del playlist.items[pos]
|
||||||
|
kodi_item = {'id': kodi_id, 'type': kodi_type, 'file': file}
|
||||||
|
add_listitem_to_Kodi_playlist(playlist,
|
||||||
|
pos,
|
||||||
|
listitem,
|
||||||
|
file,
|
||||||
|
kodi_item=kodi_item)
|
||||||
|
|
||||||
|
|
||||||
|
def add_item_to_playlist(playlist, pos, kodi_id=None, kodi_type=None,
|
||||||
|
plex_id=None, file=None):
|
||||||
|
"""
|
||||||
|
Adds an item to BOTH the Kodi and Plex playlist at position pos [int]
|
||||||
|
"""
|
||||||
|
log.debug('add_item_to_playlist. Playlist before adding: %s' % playlist)
|
||||||
|
kodi_item = {'id': kodi_id, 'type': kodi_type, 'file': file}
|
||||||
|
if playlist.ID is None:
|
||||||
|
init_Plex_playlist(playlist, plex_id, kodi_item)
|
||||||
|
else:
|
||||||
|
add_item_to_PMS_playlist(playlist, pos, plex_id, kodi_item)
|
||||||
|
kodi_id = playlist.items[pos].kodi_id
|
||||||
|
kodi_type = playlist.items[pos].kodi_type
|
||||||
|
file = playlist.items[pos].file
|
||||||
|
add_item_to_kodi_playlist(playlist, pos, kodi_id, kodi_type, file)
|
||||||
|
|
||||||
|
|
||||||
|
def add_item_to_PMS_playlist(playlist, pos, plex_id=None, kodi_item=None):
|
||||||
|
"""
|
||||||
|
Adds a new item to the playlist at position pos [int] only on the Plex
|
||||||
|
side of things (e.g. because the user changed the Kodi side)
|
||||||
|
|
||||||
|
WILL ALSO UPDATE OUR PLAYLISTS
|
||||||
|
"""
|
||||||
|
log.debug('Adding new item plex_id: %s, kodi_item: %s on the Plex side at '
|
||||||
|
'position %s for %s' % (plex_id, kodi_item, pos, playlist))
|
||||||
|
if plex_id:
|
||||||
|
item = playlist_item_from_plex(plex_id)
|
||||||
|
else:
|
||||||
|
item = playlist_item_from_kodi(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 Plex playlist
|
||||||
|
xml = DU().downloadUrl(url, action_type="PUT")
|
||||||
|
try:
|
||||||
|
item.ID = xml[-1].attrib['%sItemID' % playlist.kind]
|
||||||
|
except IndexError:
|
||||||
|
log.info('Could not get playlist children. Adding a dummy')
|
||||||
|
except (TypeError, AttributeError, KeyError):
|
||||||
|
log.error('Could not add item %s to playlist %s'
|
||||||
|
% (kodi_item, playlist))
|
||||||
|
_log_xml(xml)
|
||||||
|
return
|
||||||
|
# Get the guid for this item
|
||||||
|
for plex_item in xml:
|
||||||
|
if plex_item.attrib['%sItemID' % playlist.kind] == item.ID:
|
||||||
|
item.guid = plex_item.attrib['guid']
|
||||||
|
playlist.items.append(item)
|
||||||
|
if 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,
|
||||||
|
pos)
|
||||||
|
log.debug('Successfully added item on the Plex side: %s' % playlist)
|
||||||
|
|
||||||
|
|
||||||
|
def add_item_to_kodi_playlist(playlist, pos, kodi_id=None, kodi_type=None,
|
||||||
|
file=None):
|
||||||
|
"""
|
||||||
|
Adds an item to the KODI playlist only
|
||||||
|
|
||||||
|
WILL ALSO UPDATE OUR PLAYLISTS
|
||||||
|
"""
|
||||||
|
log.debug('Adding new item kodi_id: %s, kodi_type: %s, file: %s to Kodi '
|
||||||
|
'only at position %s for %s'
|
||||||
|
% (kodi_id, kodi_type, file, pos, playlist))
|
||||||
|
params = {
|
||||||
|
'playlistid': playlist.playlistid,
|
||||||
|
'position': pos
|
||||||
|
}
|
||||||
|
if kodi_id is not None:
|
||||||
|
params['item'] = {'%sid' % kodi_type: int(kodi_id)}
|
||||||
|
else:
|
||||||
|
params['item'] = {'file': file}
|
||||||
|
log.debug(JSONRPC('Playlist.Insert').execute(params))
|
||||||
|
playlist.items.insert(pos, playlist_item_from_kodi(
|
||||||
|
{'id': kodi_id, 'type': kodi_type, 'file': file}))
|
||||||
|
|
||||||
|
|
||||||
|
def move_playlist_item(playlist, before_pos, after_pos):
|
||||||
|
"""
|
||||||
|
Moves playlist item from before_pos [int] to after_pos [int] for Plex only.
|
||||||
|
|
||||||
|
WILL ALSO CHANGE OUR PLAYLISTS
|
||||||
|
"""
|
||||||
|
log.debug('Moving item from %s to %s on the Plex side for %s'
|
||||||
|
% (before_pos, after_pos, playlist))
|
||||||
|
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))
|
||||||
|
log.debug('Done moving for %s' % playlist)
|
||||||
|
|
||||||
|
|
||||||
|
def get_PMS_playlist(playlist, playlist_id=None):
|
||||||
|
"""
|
||||||
|
Fetches the PMS playlist/playqueue as an XML. Pass in playlist_id if we
|
||||||
|
need to fetch a new playlist
|
||||||
|
|
||||||
|
Returns None if something went wrong
|
||||||
|
"""
|
||||||
|
playlist_id = playlist_id if playlist_id else playlist.ID
|
||||||
|
xml = DU().downloadUrl(
|
||||||
|
"{server}/%ss/%s" % (playlist.kind, playlist_id),
|
||||||
|
headerOptions={'Accept': 'application/xml'})
|
||||||
|
try:
|
||||||
|
xml.attrib['%sID' % playlist.kind]
|
||||||
|
except (AttributeError, KeyError):
|
||||||
|
xml = None
|
||||||
|
return xml
|
||||||
|
|
||||||
|
|
||||||
|
def refresh_playlist_from_PMS(playlist):
|
||||||
|
"""
|
||||||
|
Only updates the selected item from the PMS side (e.g.
|
||||||
|
playQueueSelectedItemID). Will NOT check whether items still make sense.
|
||||||
|
"""
|
||||||
|
xml = get_PMS_playlist(playlist)
|
||||||
|
try:
|
||||||
|
xml.attrib['%sVersion' % playlist.kind]
|
||||||
|
except:
|
||||||
|
log.error('Could not download Plex playlist.')
|
||||||
|
return
|
||||||
|
get_playlist_details_from_xml(playlist, xml)
|
||||||
|
|
||||||
|
|
||||||
|
def delete_playlist_item_from_PMS(playlist, pos):
|
||||||
|
"""
|
||||||
|
Delete the item at position pos [int] on the Plex side and our playlists
|
||||||
|
"""
|
||||||
|
log.debug('Deleting position %s for %s on the Plex side' % (pos, playlist))
|
||||||
|
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 add_to_Kodi_playlist(playlist, xml_video_element):
|
||||||
|
"""
|
||||||
|
Adds a new item to the Kodi playlist via JSON (at the end of the playlist).
|
||||||
|
Pass in the PMS xml's video element (one level underneath MediaContainer).
|
||||||
|
|
||||||
|
Returns a Playlist_Item
|
||||||
|
"""
|
||||||
|
item = playlist_item_from_xml(playlist, xml_video_element)
|
||||||
|
params = {
|
||||||
|
'playlistid': playlist.playlistid
|
||||||
|
}
|
||||||
|
if item.kodi_id:
|
||||||
|
params['item'] = {'%sid' % item.kodi_type: item.kodi_id}
|
||||||
|
else:
|
||||||
|
params['item'] = {'file': tryEncode(item.file)}
|
||||||
|
log.debug(JSONRPC('Playlist.Add').execute(params))
|
||||||
|
return item
|
||||||
|
|
||||||
|
|
||||||
|
def add_listitem_to_Kodi_playlist(playlist, pos, listitem, file,
|
||||||
|
xml_video_element=None, kodi_item=None):
|
||||||
|
"""
|
||||||
|
Adds an xbmc listitem to the Kodi playlist.xml_video_element
|
||||||
|
|
||||||
|
WILL NOT UPDATE THE PLEX SIDE, BUT WILL UPDATE OUR PLAYLISTS
|
||||||
|
"""
|
||||||
|
log.debug('Insert listitem at position %s for Kodi only for %s'
|
||||||
|
% (pos, playlist))
|
||||||
|
# Add the item into Kodi playlist
|
||||||
|
playlist.kodi_pl.add(file, listitem, index=pos)
|
||||||
|
# We need to add this to our internal queue as well
|
||||||
|
if xml_video_element is not None:
|
||||||
|
item = playlist_item_from_xml(playlist, xml_video_element)
|
||||||
|
item.file = file
|
||||||
|
else:
|
||||||
|
item = playlist_item_from_kodi(kodi_item)
|
||||||
|
playlist.items.insert(pos, item)
|
||||||
|
log.debug('Done inserting for %s' % playlist)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_from_Kodi_playlist(playlist, pos):
|
||||||
|
"""
|
||||||
|
Removes the item at position pos from the Kodi playlist using JSON.
|
||||||
|
|
||||||
|
WILL NOT UPDATE THE PLEX SIDE, BUT WILL UPDATE OUR PLAYLISTS
|
||||||
|
"""
|
||||||
|
log.debug('Removing position %s from Kodi only from %s' % (pos, playlist))
|
||||||
|
log.debug(JSONRPC('Playlist.Remove').execute({
|
||||||
|
'playlistid': playlist.playlistid,
|
||||||
|
'position': pos
|
||||||
|
}))
|
||||||
|
del playlist.items[pos]
|
185
resources/lib/playqueue.py
Normal file
185
resources/lib/playqueue.py
Normal file
|
@ -0,0 +1,185 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
###############################################################################
|
||||||
|
import logging
|
||||||
|
from threading import RLock, Thread
|
||||||
|
|
||||||
|
from xbmc import sleep, Player, PlayList, PLAYLIST_MUSIC, PLAYLIST_VIDEO
|
||||||
|
|
||||||
|
from utils import window, ThreadMethods, ThreadMethodsAdditionalSuspend
|
||||||
|
import playlist_func as PL
|
||||||
|
from PlexFunctions import ConvertPlexToKodiTime
|
||||||
|
from playbackutils import PlaybackUtils
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
log = logging.getLogger("PLEX."+__name__)
|
||||||
|
|
||||||
|
# Lock used for playqueue manipulations
|
||||||
|
lock = RLock()
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
||||||
|
def __init__(self, callback=None):
|
||||||
|
self.__dict__ = self.__shared_state
|
||||||
|
if self.playqueues is not None:
|
||||||
|
return
|
||||||
|
self.mgr = callback
|
||||||
|
|
||||||
|
# Initialize Kodi playqueues
|
||||||
|
with lock:
|
||||||
|
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 = PlayList(PLAYLIST_MUSIC)
|
||||||
|
elif playqueue.type == 'video':
|
||||||
|
playqueue.kodi_pl = PlayList(PLAYLIST_VIDEO)
|
||||||
|
else:
|
||||||
|
# Currently, only video or audio playqueues available
|
||||||
|
playqueue.kodi_pl = PlayList(PLAYLIST_VIDEO)
|
||||||
|
self.playqueues.append(playqueue)
|
||||||
|
# sort the list by their playlistid, just in case
|
||||||
|
self.playqueues = sorted(
|
||||||
|
self.playqueues, key=lambda i: i.playlistid)
|
||||||
|
log.debug('Initialized the Kodi play queues: %s' % self.playqueues)
|
||||||
|
Thread.__init__(self)
|
||||||
|
|
||||||
|
def get_playqueue_from_type(self, typus):
|
||||||
|
"""
|
||||||
|
Returns the playqueue according to the typus ('video', 'audio',
|
||||||
|
'picture') passed in
|
||||||
|
"""
|
||||||
|
with lock:
|
||||||
|
for playqueue in self.playqueues:
|
||||||
|
if playqueue.type == typus:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise ValueError('Wrong playlist type passed in: %s' % typus)
|
||||||
|
return playqueue
|
||||||
|
|
||||||
|
def update_playqueue_from_PMS(self,
|
||||||
|
playqueue,
|
||||||
|
playqueue_id=None,
|
||||||
|
repeat=None,
|
||||||
|
offset=None):
|
||||||
|
"""
|
||||||
|
Completely updates the Kodi playqueue with the new Plex playqueue. Pass
|
||||||
|
in playqueue_id if we need to fetch a new playqueue
|
||||||
|
|
||||||
|
repeat = 0, 1, 2
|
||||||
|
offset = time offset in Plextime (milliseconds)
|
||||||
|
"""
|
||||||
|
log.info('New playqueue %s received from Plex companion with offset '
|
||||||
|
'%s, repeat %s' % (playqueue_id, offset, repeat))
|
||||||
|
with lock:
|
||||||
|
xml = PL.get_PMS_playlist(playqueue, playqueue_id)
|
||||||
|
if xml is None:
|
||||||
|
log.error('Could not get playqueue ID %s' % playqueue_id)
|
||||||
|
return
|
||||||
|
playqueue.clear()
|
||||||
|
PL.get_playlist_details_from_xml(playqueue, xml)
|
||||||
|
PlaybackUtils(xml, playqueue).play_all()
|
||||||
|
playqueue.repeat = 0 if not repeat else int(repeat)
|
||||||
|
window('plex_customplaylist', value="true")
|
||||||
|
if offset not in (None, "0"):
|
||||||
|
window('plex_customplaylist.seektime',
|
||||||
|
str(ConvertPlexToKodiTime(offset)))
|
||||||
|
for startpos, item in enumerate(playqueue.items):
|
||||||
|
if item.ID == playqueue.selectedItemID:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
startpos = 0
|
||||||
|
# Start playback. Player does not return in time
|
||||||
|
log.debug('Playqueues after Plex Companion update are now: %s'
|
||||||
|
% self.playqueues)
|
||||||
|
thread = Thread(target=Player().play,
|
||||||
|
args=(playqueue.kodi_pl,
|
||||||
|
None,
|
||||||
|
False,
|
||||||
|
startpos))
|
||||||
|
thread.setDaemon(True)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
def _compare_playqueues(self, playqueue, new):
|
||||||
|
"""
|
||||||
|
Used to poll the Kodi playqueue and update the Plex playqueue if needed
|
||||||
|
"""
|
||||||
|
old = list(playqueue.items)
|
||||||
|
index = list(range(0, len(old)))
|
||||||
|
log.debug('Comparing new Kodi playqueue %s with our play queue %s'
|
||||||
|
% (new, old))
|
||||||
|
for i, new_item in enumerate(new):
|
||||||
|
for j, old_item in enumerate(old):
|
||||||
|
if self.threadStopped():
|
||||||
|
# Chances are that we got an empty Kodi playlist due to
|
||||||
|
# Kodi exit
|
||||||
|
return
|
||||||
|
if new_item.get('id') is None:
|
||||||
|
identical = old_item.file == new_item['file']
|
||||||
|
else:
|
||||||
|
identical = (old_item.kodi_id == new_item['id'] and
|
||||||
|
old_item.kodi_type == new_item['type'])
|
||||||
|
if j == 0 and identical:
|
||||||
|
del old[j], index[j]
|
||||||
|
break
|
||||||
|
elif identical:
|
||||||
|
log.debug('Detected playqueue item %s moved to position %s'
|
||||||
|
% (i+j, i))
|
||||||
|
PL.move_playlist_item(playqueue, i + j, i)
|
||||||
|
del old[j], index[j]
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
log.debug('Detected new Kodi element at position %s: %s '
|
||||||
|
% (i, new_item))
|
||||||
|
if playqueue.ID is None:
|
||||||
|
PL.init_Plex_playlist(playqueue,
|
||||||
|
kodi_item=new_item)
|
||||||
|
else:
|
||||||
|
PL.add_item_to_PMS_playlist(playqueue,
|
||||||
|
i,
|
||||||
|
kodi_item=new_item)
|
||||||
|
index.insert(i, i)
|
||||||
|
for j in range(i+1, len(index)):
|
||||||
|
index[j] += 1
|
||||||
|
for i in reversed(index):
|
||||||
|
log.debug('Detected deletion of playqueue element at pos %s' % i)
|
||||||
|
PL.delete_playlist_item_from_PMS(playqueue, i)
|
||||||
|
log.debug('Done comparing playqueues')
|
||||||
|
|
||||||
|
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_item_to_PMS_playlist(playqueue, i, kodi_item=item)
|
||||||
|
while not threadStopped():
|
||||||
|
while threadSuspended():
|
||||||
|
if threadStopped():
|
||||||
|
break
|
||||||
|
sleep(1000)
|
||||||
|
with lock:
|
||||||
|
for playqueue in self.playqueues:
|
||||||
|
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)
|
||||||
|
sleep(50)
|
||||||
|
log.info("----===## PlayQueue client stopped ##===----")
|
|
@ -45,14 +45,14 @@ class PlayUtils():
|
||||||
log.info("File is direct playing.")
|
log.info("File is direct playing.")
|
||||||
playurl = tryEncode(playurl)
|
playurl = tryEncode(playurl)
|
||||||
# Set playmethod property
|
# Set playmethod property
|
||||||
window('emby_%s.playmethod' % playurl, "DirectPlay")
|
window('plex_%s.playmethod' % playurl, "DirectPlay")
|
||||||
|
|
||||||
elif self.isDirectStream():
|
elif self.isDirectStream():
|
||||||
log.info("File is direct streaming.")
|
log.info("File is direct streaming.")
|
||||||
playurl = tryEncode(
|
playurl = tryEncode(
|
||||||
self.API.getTranscodeVideoPath('DirectStream'))
|
self.API.getTranscodeVideoPath('DirectStream'))
|
||||||
# Set playmethod property
|
# Set playmethod property
|
||||||
window('emby_%s.playmethod' % playurl, "DirectStream")
|
window('plex_%s.playmethod' % playurl, "DirectStream")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
log.info("File is transcoding.")
|
log.info("File is transcoding.")
|
||||||
|
@ -64,7 +64,7 @@ class PlayUtils():
|
||||||
'videoQuality': '100'
|
'videoQuality': '100'
|
||||||
}))
|
}))
|
||||||
# Set playmethod property
|
# Set playmethod property
|
||||||
window('emby_%s.playmethod' % playurl, value="Transcode")
|
window('plex_%s.playmethod' % playurl, value="Transcode")
|
||||||
|
|
||||||
log.info("The playurl is: %s" % playurl)
|
log.info("The playurl is: %s" % playurl)
|
||||||
return playurl
|
return playurl
|
||||||
|
|
|
@ -5,7 +5,7 @@ import string
|
||||||
|
|
||||||
import xbmc
|
import xbmc
|
||||||
|
|
||||||
import embydb_functions as embydb
|
import plexdb_functions as plexdb
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
@ -146,11 +146,11 @@ class jsonClass():
|
||||||
def skipTo(self, plexId, typus):
|
def skipTo(self, plexId, typus):
|
||||||
# playlistId = self.getPlaylistId(tryDecode(xbmc_type(typus)))
|
# playlistId = self.getPlaylistId(tryDecode(xbmc_type(typus)))
|
||||||
# playerId = self.
|
# playerId = self.
|
||||||
with embydb.GetEmbyDB() as emby_db:
|
with plexdb.Get_Plex_DB() as plex_db:
|
||||||
embydb_item = emby_db.getItem_byId(plexId)
|
plexdb_item = plex_db.getItem_byId(plexId)
|
||||||
try:
|
try:
|
||||||
dbid = embydb_item[0]
|
dbid = plexdb_item[0]
|
||||||
mediatype = embydb_item[4]
|
mediatype = plexdb_item[4]
|
||||||
except TypeError:
|
except TypeError:
|
||||||
log.info('Couldnt find item %s in Kodi db' % plexId)
|
log.info('Couldnt find item %s in Kodi db' % plexId)
|
||||||
return
|
return
|
||||||
|
@ -163,7 +163,7 @@ class jsonClass():
|
||||||
"Access-Control-Allow-Origin": "*",
|
"Access-Control-Allow-Origin": "*",
|
||||||
"X-Plex-Version": self.settings['version'],
|
"X-Plex-Version": self.settings['version'],
|
||||||
"X-Plex-Client-Identifier": self.settings['uuid'],
|
"X-Plex-Client-Identifier": self.settings['uuid'],
|
||||||
"X-Plex-Provides": "player",
|
"X-Plex-Provides": "client,controller,player",
|
||||||
"X-Plex-Product": "PlexKodiConnect",
|
"X-Plex-Product": "PlexKodiConnect",
|
||||||
"X-Plex-Device-Name": self.settings['client_name'],
|
"X-Plex-Device-Name": self.settings['client_name'],
|
||||||
"X-Plex-Platform": "Kodi",
|
"X-Plex-Platform": "Kodi",
|
||||||
|
|
|
@ -72,7 +72,7 @@ class plexgdm:
|
||||||
"Protocol: plex\r\n"
|
"Protocol: plex\r\n"
|
||||||
"Protocol-Version: 1\r\n"
|
"Protocol-Version: 1\r\n"
|
||||||
"Protocol-Capabilities: timeline,playback,navigation,"
|
"Protocol-Capabilities: timeline,playback,navigation,"
|
||||||
"mirror,playqueues\r\n"
|
"playqueues\r\n"
|
||||||
"Device-Class: HTPC"
|
"Device-Class: HTPC"
|
||||||
) % (
|
) % (
|
||||||
options['uuid'],
|
options['uuid'],
|
||||||
|
|
|
@ -15,7 +15,7 @@ log = logging.getLogger("PLEX."+__name__)
|
||||||
|
|
||||||
|
|
||||||
class SubscriptionManager:
|
class SubscriptionManager:
|
||||||
def __init__(self, jsonClass, RequestMgr, player, playlist):
|
def __init__(self, jsonClass, RequestMgr, player, mgr):
|
||||||
self.serverlist = []
|
self.serverlist = []
|
||||||
self.subscribers = {}
|
self.subscribers = {}
|
||||||
self.info = {}
|
self.info = {}
|
||||||
|
@ -36,7 +36,7 @@ class SubscriptionManager:
|
||||||
self.playerprops = {}
|
self.playerprops = {}
|
||||||
self.doUtils = downloadutils.DownloadUtils().downloadUrl
|
self.doUtils = downloadutils.DownloadUtils().downloadUrl
|
||||||
self.xbmcplayer = player
|
self.xbmcplayer = player
|
||||||
self.playlist = playlist
|
self.playqueue = mgr.playqueue
|
||||||
|
|
||||||
self.js = jsonClass
|
self.js = jsonClass
|
||||||
self.RequestMgr = RequestMgr
|
self.RequestMgr = RequestMgr
|
||||||
|
@ -160,8 +160,8 @@ class SubscriptionManager:
|
||||||
with threading.RLock():
|
with threading.RLock():
|
||||||
for sub in self.subscribers.values():
|
for sub in self.subscribers.values():
|
||||||
sub.send_update(msg, len(players) == 0)
|
sub.send_update(msg, len(players) == 0)
|
||||||
self.notifyServer(players)
|
self.notifyServer(players)
|
||||||
self.lastplayers = players
|
self.lastplayers = players
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def notifyServer(self, players):
|
def notifyServer(self, players):
|
||||||
|
@ -231,6 +231,8 @@ class SubscriptionManager:
|
||||||
|
|
||||||
def getPlayerProperties(self, playerid):
|
def getPlayerProperties(self, playerid):
|
||||||
try:
|
try:
|
||||||
|
# Get the playqueue
|
||||||
|
playqueue = self.playqueue.playqueues[playerid]
|
||||||
# get info from the player
|
# get info from the player
|
||||||
props = self.js.jsonrpc(
|
props = self.js.jsonrpc(
|
||||||
"Player.GetProperties",
|
"Player.GetProperties",
|
||||||
|
@ -248,18 +250,16 @@ class SubscriptionManager:
|
||||||
'shuffle': ("0", "1")[props.get('shuffled', False)],
|
'shuffle': ("0", "1")[props.get('shuffled', False)],
|
||||||
'repeat': pf.getPlexRepeat(props.get('repeat')),
|
'repeat': pf.getPlexRepeat(props.get('repeat')),
|
||||||
}
|
}
|
||||||
if self.playlist is not None:
|
if playqueue.ID is not None:
|
||||||
if self.playlist.QueueId() is not None:
|
info['playQueueID'] = playqueue.ID
|
||||||
info['playQueueID'] = self.playlist.QueueId()
|
info['playQueueVersion'] = playqueue.version
|
||||||
info['playQueueVersion'] = self.playlist.PlayQueueVersion()
|
# Get the playlist position
|
||||||
info['guid'] = self.playlist.Guid()
|
pos = self.js.jsonrpc(
|
||||||
# Get the playlist position
|
"Player.GetProperties",
|
||||||
pos = self.js.jsonrpc(
|
{"playerid": playerid,
|
||||||
"Player.GetProperties",
|
"properties": ["position"]})['position']
|
||||||
{"playerid": playerid,
|
info['playQueueItemID'] = playqueue.items[pos].ID
|
||||||
"properties": ["position"]})
|
info['guid'] = playqueue.items[pos].guid
|
||||||
info['playQueueItemID'] = \
|
|
||||||
self.playlist.getQueueIdFromPosition(pos['position'])
|
|
||||||
except:
|
except:
|
||||||
import traceback
|
import traceback
|
||||||
log.error("Traceback:\n%s" % traceback.format_exc())
|
log.error("Traceback:\n%s" % traceback.format_exc())
|
||||||
|
|
391
resources/lib/plexdb_functions.py
Normal file
391
resources/lib/plexdb_functions.py
Normal file
|
@ -0,0 +1,391 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
from utils import kodiSQL
|
||||||
|
import logging
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
log = logging.getLogger("PLEX."+__name__)
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
|
||||||
|
class Get_Plex_DB():
|
||||||
|
"""
|
||||||
|
Usage: with Get_Plex_DB() as plex_db:
|
||||||
|
plex_db.do_something()
|
||||||
|
|
||||||
|
On exiting "with" (no matter what), commits get automatically committed
|
||||||
|
and the db gets closed
|
||||||
|
"""
|
||||||
|
def __enter__(self):
|
||||||
|
self.plexconn = kodiSQL('plex')
|
||||||
|
return Plex_DB_Functions(self.plexconn.cursor())
|
||||||
|
|
||||||
|
def __exit__(self, type, value, traceback):
|
||||||
|
self.plexconn.commit()
|
||||||
|
self.plexconn.close()
|
||||||
|
|
||||||
|
|
||||||
|
class Plex_DB_Functions():
|
||||||
|
|
||||||
|
def __init__(self, plexcursor):
|
||||||
|
self.plexcursor = plexcursor
|
||||||
|
|
||||||
|
def getViews(self):
|
||||||
|
"""
|
||||||
|
Returns a list of view_id
|
||||||
|
"""
|
||||||
|
views = []
|
||||||
|
query = '''
|
||||||
|
SELECT view_id
|
||||||
|
FROM view
|
||||||
|
'''
|
||||||
|
self.plexcursor.execute(query)
|
||||||
|
rows = self.plexcursor.fetchall()
|
||||||
|
for row in rows:
|
||||||
|
views.append(row[0])
|
||||||
|
return views
|
||||||
|
|
||||||
|
def getAllViewInfo(self):
|
||||||
|
"""
|
||||||
|
Returns a list of dicts:
|
||||||
|
{'id': view_id, 'name': view_name, 'itemtype': kodi_type}
|
||||||
|
"""
|
||||||
|
plexcursor = self.plexcursor
|
||||||
|
views = []
|
||||||
|
query = '''
|
||||||
|
SELECT view_id, view_name, kodi_type
|
||||||
|
FROM view
|
||||||
|
'''
|
||||||
|
plexcursor.execute(query)
|
||||||
|
rows = plexcursor.fetchall()
|
||||||
|
for row in rows:
|
||||||
|
views.append({'id': row[0],
|
||||||
|
'name': row[1],
|
||||||
|
'itemtype': row[2]})
|
||||||
|
return views
|
||||||
|
|
||||||
|
def getView_byId(self, view_id):
|
||||||
|
"""
|
||||||
|
Returns tuple (view_name, kodi_type, kodi_tagid) for view_id
|
||||||
|
"""
|
||||||
|
query = '''
|
||||||
|
SELECT view_name, kodi_type, kodi_tagid
|
||||||
|
FROM view
|
||||||
|
WHERE view_id = ?
|
||||||
|
'''
|
||||||
|
self.plexcursor.execute(query, (view_id,))
|
||||||
|
view = self.plexcursor.fetchone()
|
||||||
|
return view
|
||||||
|
|
||||||
|
def getView_byType(self, kodi_type):
|
||||||
|
"""
|
||||||
|
Returns a list of dicts for kodi_type:
|
||||||
|
{'id': view_id, 'name': view_name, 'itemtype': kodi_type}
|
||||||
|
"""
|
||||||
|
views = []
|
||||||
|
query = '''
|
||||||
|
SELECT view_id, view_name, kodi_type
|
||||||
|
FROM view
|
||||||
|
WHERE kodi_type = ?
|
||||||
|
'''
|
||||||
|
self.plexcursor.execute(query, (kodi_type,))
|
||||||
|
rows = self.plexcursor.fetchall()
|
||||||
|
for row in rows:
|
||||||
|
views.append({
|
||||||
|
'id': row[0],
|
||||||
|
'name': row[1],
|
||||||
|
'itemtype': row[2]
|
||||||
|
})
|
||||||
|
return views
|
||||||
|
|
||||||
|
def getView_byName(self, view_name):
|
||||||
|
"""
|
||||||
|
Returns the view_id for view_name (or None)
|
||||||
|
"""
|
||||||
|
query = '''
|
||||||
|
SELECT view_id
|
||||||
|
FROM view
|
||||||
|
WHERE view_name = ?
|
||||||
|
'''
|
||||||
|
self.plexcursor.execute(query, (view_name,))
|
||||||
|
try:
|
||||||
|
view = self.plexcursor.fetchone()[0]
|
||||||
|
except TypeError:
|
||||||
|
view = None
|
||||||
|
return view
|
||||||
|
|
||||||
|
def addView(self, view_id, view_name, kodi_type, kodi_tagid):
|
||||||
|
"""
|
||||||
|
Appends an entry to the view table
|
||||||
|
"""
|
||||||
|
query = '''
|
||||||
|
INSERT INTO view(
|
||||||
|
view_id, view_name, kodi_type, kodi_tagid)
|
||||||
|
VALUES (?, ?, ?, ?)
|
||||||
|
'''
|
||||||
|
self.plexcursor.execute(query,
|
||||||
|
(view_id, view_name, kodi_type, kodi_tagid))
|
||||||
|
|
||||||
|
def updateView(self, view_name, kodi_tagid, view_id):
|
||||||
|
"""
|
||||||
|
Updates the view_id with view_name and kodi_tagid
|
||||||
|
"""
|
||||||
|
query = '''
|
||||||
|
UPDATE view
|
||||||
|
SET view_name = ?, kodi_tagid = ?
|
||||||
|
WHERE view_id = ?
|
||||||
|
'''
|
||||||
|
self.plexcursor.execute(query, (view_name, kodi_tagid, view_id))
|
||||||
|
|
||||||
|
def removeView(self, view_id):
|
||||||
|
query = '''
|
||||||
|
DELETE FROM view
|
||||||
|
WHERE view_id = ?
|
||||||
|
'''
|
||||||
|
self.plexcursor.execute(query, (view_id,))
|
||||||
|
|
||||||
|
def getItem_byFileId(self, kodi_fileid, kodi_type):
|
||||||
|
"""
|
||||||
|
Returns plex_id for kodi_fileid and kodi_type
|
||||||
|
|
||||||
|
None if not found
|
||||||
|
"""
|
||||||
|
query = '''
|
||||||
|
SELECT plex_id
|
||||||
|
FROM plex
|
||||||
|
WHERE kodi_fileid = ? AND kodi_type = ?
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
self.plexcursor.execute(query, (kodi_fileid, kodi_type))
|
||||||
|
item = self.plexcursor.fetchone()[0]
|
||||||
|
return item
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def getMusicItem_byFileId(self, kodi_id, kodi_type):
|
||||||
|
"""
|
||||||
|
Returns the plex_id for kodi_id and kodi_type
|
||||||
|
|
||||||
|
None if not found
|
||||||
|
"""
|
||||||
|
query = '''
|
||||||
|
SELECT plex_id
|
||||||
|
FROM plex
|
||||||
|
WHERE kodi_id = ? AND kodi_type = ?
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
self.plexcursor.execute(query, (kodi_id, kodi_type))
|
||||||
|
item = self.plexcursor.fetchone()[0]
|
||||||
|
return item
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def getItem_byId(self, plex_id):
|
||||||
|
"""
|
||||||
|
For plex_id, returns the tuple
|
||||||
|
(kodi_id, kodi_fileid, kodi_pathid, parent_id, kodi_type, plex_type)
|
||||||
|
|
||||||
|
None if not found
|
||||||
|
"""
|
||||||
|
query = '''
|
||||||
|
SELECT kodi_id, kodi_fileid, kodi_pathid,
|
||||||
|
parent_id, kodi_type, plex_type
|
||||||
|
FROM plex
|
||||||
|
WHERE plex_id = ?
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
self.plexcursor.execute(query, (plex_id,))
|
||||||
|
item = self.plexcursor.fetchone()
|
||||||
|
return item
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def getItem_byWildId(self, plex_id):
|
||||||
|
"""
|
||||||
|
Returns a list of tuples (kodi_id, kodi_type) for plex_id (% appended)
|
||||||
|
"""
|
||||||
|
query = '''
|
||||||
|
SELECT kodi_id, kodi_type
|
||||||
|
FROM plex
|
||||||
|
WHERE plex_id LIKE ?
|
||||||
|
'''
|
||||||
|
self.plexcursor.execute(query, (plex_id+"%",))
|
||||||
|
return self.plexcursor.fetchall()
|
||||||
|
|
||||||
|
def getItem_byView(self, view_id):
|
||||||
|
"""
|
||||||
|
Returns kodi_id for view_id
|
||||||
|
"""
|
||||||
|
query = '''
|
||||||
|
SELECT kodi_id
|
||||||
|
FROM plex
|
||||||
|
WHERE view_id = ?
|
||||||
|
'''
|
||||||
|
self.plexcursor.execute(query, (view_id,))
|
||||||
|
return self.plexcursor.fetchall()
|
||||||
|
|
||||||
|
def getItem_byKodiId(self, kodi_id, kodi_type):
|
||||||
|
"""
|
||||||
|
Returns the tuple (plex_id, parent_id) for kodi_id and kodi_type
|
||||||
|
"""
|
||||||
|
query = '''
|
||||||
|
SELECT plex_id, parent_id
|
||||||
|
FROM plex
|
||||||
|
WHERE kodi_id = ?
|
||||||
|
AND kodi_type = ?
|
||||||
|
'''
|
||||||
|
self.plexcursor.execute(query, (kodi_id, kodi_type,))
|
||||||
|
return self.plexcursor.fetchone()
|
||||||
|
|
||||||
|
def getItem_byParentId(self, parent_id, kodi_type):
|
||||||
|
"""
|
||||||
|
Returns the tuple (plex_id, kodi_id, kodi_fileid) for parent_id,
|
||||||
|
kodi_type
|
||||||
|
"""
|
||||||
|
query = '''
|
||||||
|
SELECT plex_id, kodi_id, kodi_fileid
|
||||||
|
FROM plex
|
||||||
|
WHERE parent_id = ?
|
||||||
|
AND kodi_type = ?"
|
||||||
|
'''
|
||||||
|
self.plexcursor.execute(query, (parent_id, kodi_type,))
|
||||||
|
return self.plexcursor.fetchall()
|
||||||
|
|
||||||
|
def getItemId_byParentId(self, parent_id, kodi_type):
|
||||||
|
"""
|
||||||
|
Returns the tuple (plex_id, kodi_id) for parent_id, kodi_type
|
||||||
|
"""
|
||||||
|
query = '''
|
||||||
|
SELECT plex_id, kodi_id
|
||||||
|
FROM plex
|
||||||
|
WHERE parent_id = ?
|
||||||
|
AND kodi_type = ?
|
||||||
|
'''
|
||||||
|
self.plexcursor.execute(query, (parent_id, kodi_type,))
|
||||||
|
return self.plexcursor.fetchall()
|
||||||
|
|
||||||
|
def getChecksum(self, plex_type):
|
||||||
|
"""
|
||||||
|
Returns a list of tuples (plex_id, checksum) for plex_type
|
||||||
|
"""
|
||||||
|
query = '''
|
||||||
|
SELECT plex_id, checksum
|
||||||
|
FROM plex
|
||||||
|
WHERE plex_type = ?
|
||||||
|
'''
|
||||||
|
self.plexcursor.execute(query, (plex_type,))
|
||||||
|
return self.plexcursor.fetchall()
|
||||||
|
|
||||||
|
def getMediaType_byId(self, plex_id):
|
||||||
|
"""
|
||||||
|
Returns plex_type for plex_id
|
||||||
|
|
||||||
|
Or None if not found
|
||||||
|
"""
|
||||||
|
query = '''
|
||||||
|
SELECT plex_type
|
||||||
|
FROM plex
|
||||||
|
WHERE plex_id = ?
|
||||||
|
'''
|
||||||
|
self.plexcursor.execute(query, (plex_id,))
|
||||||
|
try:
|
||||||
|
itemtype = self.plexcursor.fetchone()[0]
|
||||||
|
except TypeError:
|
||||||
|
itemtype = None
|
||||||
|
return itemtype
|
||||||
|
|
||||||
|
def addReference(self, plex_id, plex_type, kodi_id, kodi_type,
|
||||||
|
kodi_fileid=None, kodi_pathid=None, parent_id=None,
|
||||||
|
checksum=None, view_id=None):
|
||||||
|
"""
|
||||||
|
Appends or replaces an entry into the plex table
|
||||||
|
"""
|
||||||
|
query = '''
|
||||||
|
INSERT OR REPLACE INTO plex(
|
||||||
|
plex_id, kodi_id, kodi_fileid, kodi_pathid, plex_type,
|
||||||
|
kodi_type, parent_id, checksum, view_id)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
'''
|
||||||
|
self.plexcursor.execute(query, (plex_id, kodi_id, kodi_fileid,
|
||||||
|
kodi_pathid, plex_type, kodi_type,
|
||||||
|
parent_id, checksum, view_id))
|
||||||
|
|
||||||
|
def updateReference(self, plex_id, checksum):
|
||||||
|
"""
|
||||||
|
Updates checksum for plex_id
|
||||||
|
"""
|
||||||
|
query = "UPDATE plex SET checksum = ? WHERE plex_id = ?"
|
||||||
|
self.plexcursor.execute(query, (checksum, plex_id))
|
||||||
|
|
||||||
|
def updateParentId(self, plexid, parent_kodiid):
|
||||||
|
"""
|
||||||
|
Updates parent_id for plex_id
|
||||||
|
"""
|
||||||
|
query = "UPDATE plex SET parent_id = ? WHERE plex_id = ?"
|
||||||
|
self.plexcursor.execute(query, (parent_kodiid, plexid))
|
||||||
|
|
||||||
|
def removeItems_byParentId(self, parent_id, kodi_type):
|
||||||
|
"""
|
||||||
|
Removes all entries with parent_id and kodi_type
|
||||||
|
"""
|
||||||
|
query = '''
|
||||||
|
DELETE FROM plex
|
||||||
|
WHERE parent_id = ?
|
||||||
|
AND kodi_type = ?
|
||||||
|
'''
|
||||||
|
self.plexcursor.execute(query, (parent_id, kodi_type,))
|
||||||
|
|
||||||
|
def removeItem_byKodiId(self, kodi_id, kodi_type):
|
||||||
|
"""
|
||||||
|
Removes the one entry with kodi_id and kodi_type
|
||||||
|
"""
|
||||||
|
query = '''
|
||||||
|
DELETE FROM plex
|
||||||
|
WHERE kodi_id = ?
|
||||||
|
AND kodi_type = ?
|
||||||
|
'''
|
||||||
|
self.plexcursor.execute(query, (kodi_id, kodi_type,))
|
||||||
|
|
||||||
|
def removeItem(self, plex_id):
|
||||||
|
"""
|
||||||
|
Removes the one entry with plex_id
|
||||||
|
"""
|
||||||
|
query = "DELETE FROM plex WHERE plex_id = ?"
|
||||||
|
self.plexcursor.execute(query, (plex_id,))
|
||||||
|
|
||||||
|
def removeWildItem(self, plex_id):
|
||||||
|
"""
|
||||||
|
Removes all entries with plex_id with % added
|
||||||
|
"""
|
||||||
|
query = "DELETE FROM plex WHERE plex_id LIKE ?"
|
||||||
|
self.plexcursor.execute(query, (plex_id+"%",))
|
||||||
|
|
||||||
|
def itemsByType(self, plex_type):
|
||||||
|
"""
|
||||||
|
Returns a list of dicts for plex_type:
|
||||||
|
{
|
||||||
|
'plexId': plex_id
|
||||||
|
'kodiId': kodi_id
|
||||||
|
'kodi_type': kodi_type
|
||||||
|
'plex_type': plex_type
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
query = '''
|
||||||
|
SELECT plex_id, kodi_id, kodi_type
|
||||||
|
FROM plex
|
||||||
|
WHERE plex_type = ?
|
||||||
|
'''
|
||||||
|
self.plexcursor.execute(query, (plex_type, ))
|
||||||
|
result = []
|
||||||
|
for row in self.plexcursor.fetchall():
|
||||||
|
result.append({
|
||||||
|
'plexId': row[0],
|
||||||
|
'kodiId': row[1],
|
||||||
|
'kodi_type': row[2],
|
||||||
|
'plex_type': plex_type
|
||||||
|
})
|
||||||
|
return result
|
|
@ -101,7 +101,7 @@ class Read_EmbyServer():
|
||||||
viewId = view['Id']
|
viewId = view['Id']
|
||||||
|
|
||||||
# Compare to view table in emby database
|
# Compare to view table in emby database
|
||||||
emby = kodiSQL('emby')
|
emby = kodiSQL('plex')
|
||||||
cursor_emby = emby.cursor()
|
cursor_emby = emby.cursor()
|
||||||
query = ' '.join((
|
query = ' '.join((
|
||||||
|
|
||||||
|
|
|
@ -32,8 +32,10 @@ class UserClient(threading.Thread):
|
||||||
# Borg - multiple instances, shared state
|
# Borg - multiple instances, shared state
|
||||||
__shared_state = {}
|
__shared_state = {}
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, callback=None):
|
||||||
self.__dict__ = self.__shared_state
|
self.__dict__ = self.__shared_state
|
||||||
|
if callback is not None:
|
||||||
|
self.mgr = callback
|
||||||
|
|
||||||
self.auth = True
|
self.auth = True
|
||||||
self.retry = 0
|
self.retry = 0
|
||||||
|
|
|
@ -15,7 +15,6 @@ from functools import wraps
|
||||||
from calendar import timegm
|
from calendar import timegm
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
||||||
import xbmc
|
import xbmc
|
||||||
import xbmcaddon
|
import xbmcaddon
|
||||||
import xbmcgui
|
import xbmcgui
|
||||||
|
@ -57,6 +56,24 @@ def window(property, value=None, clear=False, windowid=10000):
|
||||||
return tryDecode(win.getProperty(property))
|
return tryDecode(win.getProperty(property))
|
||||||
|
|
||||||
|
|
||||||
|
def pickl_window(property, value=None, clear=False, windowid=10000):
|
||||||
|
"""
|
||||||
|
Get or set window property - thread safe! For use with Pickle
|
||||||
|
Property and value must be string
|
||||||
|
"""
|
||||||
|
if windowid != 10000:
|
||||||
|
win = xbmcgui.Window(windowid)
|
||||||
|
else:
|
||||||
|
win = WINDOW
|
||||||
|
|
||||||
|
if clear:
|
||||||
|
win.clearProperty(property)
|
||||||
|
elif value is not None:
|
||||||
|
win.setProperty(property, value)
|
||||||
|
else:
|
||||||
|
return win.getProperty(property)
|
||||||
|
|
||||||
|
|
||||||
def settings(setting, value=None):
|
def settings(setting, value=None):
|
||||||
"""
|
"""
|
||||||
Get or add addon setting. Returns unicode
|
Get or add addon setting. Returns unicode
|
||||||
|
@ -134,21 +151,21 @@ def tryDecode(string, encoding='utf-8'):
|
||||||
|
|
||||||
|
|
||||||
def DateToKodi(stamp):
|
def DateToKodi(stamp):
|
||||||
"""
|
"""
|
||||||
converts a Unix time stamp (seconds passed sinceJanuary 1 1970) to a
|
converts a Unix time stamp (seconds passed sinceJanuary 1 1970) to a
|
||||||
propper, human-readable time stamp used by Kodi
|
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
|
None if an error was encountered
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
stamp = float(stamp) + float(window('kodiplextimeoffset'))
|
stamp = float(stamp) + float(window('kodiplextimeoffset'))
|
||||||
date_time = time.localtime(stamp)
|
date_time = time.localtime(stamp)
|
||||||
localdate = time.strftime('%Y-%m-%d %H:%M:%S', date_time)
|
localdate = time.strftime('%Y-%m-%d %H:%M:%S', date_time)
|
||||||
except:
|
except:
|
||||||
localdate = None
|
localdate = None
|
||||||
return localdate
|
return localdate
|
||||||
|
|
||||||
|
|
||||||
def IfExists(path):
|
def IfExists(path):
|
||||||
|
@ -200,8 +217,8 @@ def getUnixTimestamp(secondsIntoTheFuture=None):
|
||||||
|
|
||||||
def kodiSQL(media_type="video"):
|
def kodiSQL(media_type="video"):
|
||||||
|
|
||||||
if media_type == "emby":
|
if media_type == "plex":
|
||||||
dbPath = tryDecode(xbmc.translatePath("special://database/emby.db"))
|
dbPath = tryDecode(xbmc.translatePath("special://database/plex.db"))
|
||||||
elif media_type == "music":
|
elif media_type == "music":
|
||||||
dbPath = getKodiMusicDBPath()
|
dbPath = getKodiMusicDBPath()
|
||||||
elif media_type == "texture":
|
elif media_type == "texture":
|
||||||
|
@ -346,7 +363,7 @@ def reset():
|
||||||
|
|
||||||
# Wipe the Plex database
|
# Wipe the Plex database
|
||||||
log.info("Resetting the Plex database.")
|
log.info("Resetting the Plex database.")
|
||||||
connection = kodiSQL('emby')
|
connection = kodiSQL('plex')
|
||||||
cursor = connection.cursor()
|
cursor = connection.cursor()
|
||||||
cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"')
|
cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"')
|
||||||
rows = cursor.fetchall()
|
rows = cursor.fetchall()
|
||||||
|
@ -939,9 +956,33 @@ def ThreadMethods(cls):
|
||||||
return 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
|
# UNUSED METHODS
|
||||||
|
|
||||||
|
|
||||||
def changePlayState(itemType, kodiId, playCount, lastplayed):
|
def changePlayState(itemType, kodiId, playCount, lastplayed):
|
||||||
"""
|
"""
|
||||||
YET UNUSED
|
YET UNUSED
|
||||||
|
@ -985,3 +1026,27 @@ def changePlayState(itemType, kodiId, playCount, lastplayed):
|
||||||
result = json.loads(result)
|
result = json.loads(result)
|
||||||
result = result.get('result')
|
result = result.get('result')
|
||||||
log.debug("JSON result was: %s" % result)
|
log.debug("JSON result was: %s" % result)
|
||||||
|
|
||||||
|
|
||||||
|
class JSONRPC(object):
|
||||||
|
id_ = 1
|
||||||
|
jsonrpc = "2.0"
|
||||||
|
|
||||||
|
def __init__(self, method, **kwargs):
|
||||||
|
self.method = method
|
||||||
|
for arg in kwargs: # id_(int), jsonrpc(str)
|
||||||
|
self.arg = arg
|
||||||
|
|
||||||
|
def _query(self):
|
||||||
|
query = {
|
||||||
|
'jsonrpc': self.jsonrpc,
|
||||||
|
'id': self.id_,
|
||||||
|
'method': self.method,
|
||||||
|
}
|
||||||
|
if self.params is not None:
|
||||||
|
query['params'] = self.params
|
||||||
|
return json.dumps(query)
|
||||||
|
|
||||||
|
def execute(self, params=None):
|
||||||
|
self.params = params
|
||||||
|
return json.loads(xbmc.executeJSONRPC(self._query()))
|
||||||
|
|
|
@ -2,13 +2,13 @@
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
import logging
|
import logging
|
||||||
import json
|
|
||||||
import threading
|
|
||||||
import Queue
|
|
||||||
import websocket
|
import websocket
|
||||||
import ssl
|
from json import loads
|
||||||
|
from threading import Thread
|
||||||
|
from Queue import Queue
|
||||||
|
from ssl import CERT_NONE
|
||||||
|
|
||||||
import xbmc
|
from xbmc import sleep
|
||||||
|
|
||||||
from utils import window, settings, ThreadMethodsAdditionalSuspend, \
|
from utils import window, settings, ThreadMethodsAdditionalSuspend, \
|
||||||
ThreadMethods
|
ThreadMethods
|
||||||
|
@ -22,21 +22,23 @@ log = logging.getLogger("PLEX."+__name__)
|
||||||
|
|
||||||
@ThreadMethodsAdditionalSuspend('suspend_LibraryThread')
|
@ThreadMethodsAdditionalSuspend('suspend_LibraryThread')
|
||||||
@ThreadMethods
|
@ThreadMethods
|
||||||
class WebSocket(threading.Thread):
|
class WebSocket(Thread):
|
||||||
opcode_data = (websocket.ABNF.OPCODE_TEXT, websocket.ABNF.OPCODE_BINARY)
|
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
|
self.ws = None
|
||||||
# Communication with librarysync
|
# Communication with librarysync
|
||||||
self.queue = queue
|
self.queue = Queue()
|
||||||
threading.Thread.__init__(self)
|
Thread.__init__(self)
|
||||||
|
|
||||||
def process(self, opcode, message):
|
def process(self, opcode, message):
|
||||||
if opcode not in self.opcode_data:
|
if opcode not in self.opcode_data:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
message = json.loads(message)
|
message = loads(message)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
log.error('Error decoding message from websocket: %s' % ex)
|
log.error('Error decoding message from websocket: %s' % ex)
|
||||||
log.error(message)
|
log.error(message)
|
||||||
|
@ -57,13 +59,8 @@ class WebSocket(threading.Thread):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Put PMS message on queue and let libsync take care of it
|
# Put PMS message on queue and let libsync take care of it
|
||||||
try:
|
self.queue.put(message)
|
||||||
self.queue.put(message)
|
return True
|
||||||
return True
|
|
||||||
except Queue.Full:
|
|
||||||
# Queue only takes 200 messages. No worries if we miss one or two
|
|
||||||
log.info('Queue is full, dropping PMS message %s' % message)
|
|
||||||
return False
|
|
||||||
|
|
||||||
def receive(self, ws):
|
def receive(self, ws):
|
||||||
# Not connected yet
|
# Not connected yet
|
||||||
|
@ -97,7 +94,7 @@ class WebSocket(threading.Thread):
|
||||||
uri += '?X-Plex-Token=%s' % token
|
uri += '?X-Plex-Token=%s' % token
|
||||||
sslopt = {}
|
sslopt = {}
|
||||||
if settings('sslverify') == "false":
|
if settings('sslverify') == "false":
|
||||||
sslopt["cert_reqs"] = ssl.CERT_NONE
|
sslopt["cert_reqs"] = CERT_NONE
|
||||||
log.debug("Uri: %s, sslopt: %s" % (uri, sslopt))
|
log.debug("Uri: %s, sslopt: %s" % (uri, sslopt))
|
||||||
return uri, sslopt
|
return uri, sslopt
|
||||||
|
|
||||||
|
@ -122,7 +119,7 @@ class WebSocket(threading.Thread):
|
||||||
# Abort was requested while waiting. We should exit
|
# Abort was requested while waiting. We should exit
|
||||||
log.info("##===---- WebSocketClient Stopped ----===##")
|
log.info("##===---- WebSocketClient Stopped ----===##")
|
||||||
return
|
return
|
||||||
xbmc.sleep(1000)
|
sleep(1000)
|
||||||
try:
|
try:
|
||||||
self.process(*self.receive(self.ws))
|
self.process(*self.receive(self.ws))
|
||||||
except websocket.WebSocketTimeoutException:
|
except websocket.WebSocketTimeoutException:
|
||||||
|
@ -148,11 +145,11 @@ class WebSocket(threading.Thread):
|
||||||
"declaring the connection dead")
|
"declaring the connection dead")
|
||||||
window('plex_online', value='false')
|
window('plex_online', value='false')
|
||||||
counter = 0
|
counter = 0
|
||||||
xbmc.sleep(1000)
|
sleep(1000)
|
||||||
except websocket.WebSocketTimeoutException:
|
except websocket.WebSocketTimeoutException:
|
||||||
log.info("timeout while connecting, trying again")
|
log.info("timeout while connecting, trying again")
|
||||||
self.ws = None
|
self.ws = None
|
||||||
xbmc.sleep(1000)
|
sleep(1000)
|
||||||
except websocket.WebSocketException as e:
|
except websocket.WebSocketException as e:
|
||||||
log.info('WebSocketException: %s' % e)
|
log.info('WebSocketException: %s' % e)
|
||||||
if 'Handshake Status 401' in e.args:
|
if 'Handshake Status 401' in e.args:
|
||||||
|
@ -162,14 +159,14 @@ class WebSocket(threading.Thread):
|
||||||
'WebSocketClient now')
|
'WebSocketClient now')
|
||||||
break
|
break
|
||||||
self.ws = None
|
self.ws = None
|
||||||
xbmc.sleep(1000)
|
sleep(1000)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error("Unknown exception encountered in connecting: %s"
|
log.error("Unknown exception encountered in connecting: %s"
|
||||||
% e)
|
% e)
|
||||||
import traceback
|
import traceback
|
||||||
log.error("Traceback:\n%s" % traceback.format_exc())
|
log.error("Traceback:\n%s" % traceback.format_exc())
|
||||||
self.ws = None
|
self.ws = None
|
||||||
xbmc.sleep(1000)
|
sleep(1000)
|
||||||
else:
|
else:
|
||||||
counter = 0
|
counter = 0
|
||||||
handshake_counter = 0
|
handshake_counter = 0
|
||||||
|
|
|
@ -87,7 +87,7 @@
|
||||||
|
|
||||||
<category label="30516"><!-- Playback -->
|
<category label="30516"><!-- Playback -->
|
||||||
<setting type="sep" />
|
<setting type="sep" />
|
||||||
<setting id="enableCinema" type="bool" label="30518" default="true" />
|
<setting id="enableCinema" type="bool" label="30518" default="false" />
|
||||||
<setting id="askCinema" type="bool" label="30519" default="false" visible="eq(-1,true)" subsetting="true" />
|
<setting id="askCinema" type="bool" label="30519" default="false" visible="eq(-1,true)" subsetting="true" />
|
||||||
<setting id="trailerNumber" type="slider" label="39000" default="3" visible="eq(-2,true)" range="1,1,15" option="int" />
|
<setting id="trailerNumber" type="slider" label="39000" default="3" visible="eq(-2,true)" range="1,1,15" option="int" />
|
||||||
<setting id="pickPlexSubtitles" type="bool" label="39075" default="true" />
|
<setting id="pickPlexSubtitles" type="bool" label="39075" default="true" />
|
||||||
|
|
111
service.py
111
service.py
|
@ -5,7 +5,6 @@
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import Queue
|
|
||||||
|
|
||||||
import xbmc
|
import xbmc
|
||||||
import xbmcaddon
|
import xbmcaddon
|
||||||
|
@ -33,17 +32,20 @@ sys.path.append(_base_resource)
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
from utils import settings, window, language as lang
|
from utils import settings, window, language as lang
|
||||||
import userclient
|
from userclient import UserClient
|
||||||
import clientinfo
|
import clientinfo
|
||||||
import initialsetup
|
import initialsetup
|
||||||
import kodimonitor
|
from kodimonitor import KodiMonitor
|
||||||
import librarysync
|
from librarysync import LibrarySync
|
||||||
import videonodes
|
import videonodes
|
||||||
import websocket_client as wsc
|
from websocket_client import WebSocket
|
||||||
import downloadutils
|
import downloadutils
|
||||||
|
from playqueue import Playqueue
|
||||||
|
|
||||||
import PlexAPI
|
import PlexAPI
|
||||||
import PlexCompanion
|
from PlexCompanion import PlexCompanion
|
||||||
|
from monitor_kodi_play import Monitor_Kodi_Play
|
||||||
|
from playback_starter import Playback_Starter
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
@ -61,11 +63,19 @@ class Service():
|
||||||
server_online = True
|
server_online = True
|
||||||
warn_auth = True
|
warn_auth = True
|
||||||
|
|
||||||
userclient_running = False
|
user = None
|
||||||
websocket_running = False
|
ws = None
|
||||||
|
library = None
|
||||||
|
plexCompanion = None
|
||||||
|
playqueue = None
|
||||||
|
|
||||||
|
user_running = False
|
||||||
|
ws_running = False
|
||||||
library_running = False
|
library_running = False
|
||||||
kodimonitor_running = False
|
|
||||||
plexCompanion_running = False
|
plexCompanion_running = False
|
||||||
|
playqueue_running = False
|
||||||
|
kodimonitor_running = False
|
||||||
|
playback_starter_running = False
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
||||||
|
@ -96,13 +106,14 @@ class Service():
|
||||||
"plex_online", "plex_serverStatus", "plex_onWake",
|
"plex_online", "plex_serverStatus", "plex_onWake",
|
||||||
"plex_dbCheck", "plex_kodiScan",
|
"plex_dbCheck", "plex_kodiScan",
|
||||||
"plex_shouldStop", "currUserId", "plex_dbScan",
|
"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",
|
"plex_runLibScan", "plex_username", "pms_token", "plex_token",
|
||||||
"pms_server", "plex_machineIdentifier", "plex_servername",
|
"pms_server", "plex_machineIdentifier", "plex_servername",
|
||||||
"plex_authenticated", "PlexUserImage", "useDirectPaths",
|
"plex_authenticated", "PlexUserImage", "useDirectPaths",
|
||||||
"suspend_LibraryThread", "plex_terminateNow",
|
"suspend_LibraryThread", "plex_terminateNow",
|
||||||
"kodiplextimeoffset", "countError", "countUnauthorized",
|
"kodiplextimeoffset", "countError", "countUnauthorized",
|
||||||
"plex_restricteduser", "plex_allows_mediaDeletion"
|
"plex_restricteduser", "plex_allows_mediaDeletion",
|
||||||
|
"plex_play_new_item", "plex_result"
|
||||||
]
|
]
|
||||||
for prop in properties:
|
for prop in properties:
|
||||||
window(prop, clear=True)
|
window(prop, clear=True)
|
||||||
|
@ -111,7 +122,7 @@ class Service():
|
||||||
videonodes.VideoNodes().clearProperties()
|
videonodes.VideoNodes().clearProperties()
|
||||||
|
|
||||||
# Set the minimum database version
|
# Set the minimum database version
|
||||||
window('plex_minDBVersion', value="1.1.5")
|
window('plex_minDBVersion', value="1.5.2")
|
||||||
|
|
||||||
def getLogLevel(self):
|
def getLogLevel(self):
|
||||||
try:
|
try:
|
||||||
|
@ -126,16 +137,21 @@ class Service():
|
||||||
monitor = self.monitor
|
monitor = self.monitor
|
||||||
kodiProfile = xbmc.translatePath("special://profile")
|
kodiProfile = xbmc.translatePath("special://profile")
|
||||||
|
|
||||||
|
# Detect playback start early on
|
||||||
|
self.monitor_kodi_play = Monitor_Kodi_Play(self)
|
||||||
|
self.monitor_kodi_play.start()
|
||||||
|
|
||||||
# Server auto-detect
|
# Server auto-detect
|
||||||
initialsetup.InitialSetup().setup()
|
initialsetup.InitialSetup().setup()
|
||||||
|
|
||||||
# Queue for background sync
|
# Initialize important threads, handing over self for callback purposes
|
||||||
queue = Queue.Queue()
|
self.user = UserClient(self)
|
||||||
|
self.ws = WebSocket(self)
|
||||||
|
self.library = LibrarySync(self)
|
||||||
|
self.plexCompanion = PlexCompanion(self)
|
||||||
|
self.playqueue = Playqueue(self)
|
||||||
|
self.playback_starter = Playback_Starter(self)
|
||||||
|
|
||||||
# Initialize important threads
|
|
||||||
user = userclient.UserClient()
|
|
||||||
ws = wsc.WebSocket(queue)
|
|
||||||
library = librarysync.LibrarySync(queue)
|
|
||||||
plx = PlexAPI.PlexAPI()
|
plx = PlexAPI.PlexAPI()
|
||||||
|
|
||||||
welcome_msg = True
|
welcome_msg = True
|
||||||
|
@ -157,7 +173,7 @@ class Service():
|
||||||
if window('plex_online') == "true":
|
if window('plex_online') == "true":
|
||||||
# Plex server is online
|
# Plex server is online
|
||||||
# Verify if user is set and has access to the server
|
# 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:
|
if not self.kodimonitor_running:
|
||||||
# Start up events
|
# Start up events
|
||||||
self.warn_auth = True
|
self.warn_auth = True
|
||||||
|
@ -166,38 +182,46 @@ class Service():
|
||||||
welcome_msg = False
|
welcome_msg = False
|
||||||
xbmcgui.Dialog().notification(
|
xbmcgui.Dialog().notification(
|
||||||
heading=addonName,
|
heading=addonName,
|
||||||
message="%s %s" % (lang(33000), user.currUser),
|
message="%s %s" % (lang(33000),
|
||||||
icon="special://home/addons/plugin.video.plexkodiconnect/icon.png",
|
self.user.currUser),
|
||||||
|
icon="special://home/addons/plugin."
|
||||||
|
"video.plexkodiconnect/icon.png",
|
||||||
time=2000,
|
time=2000,
|
||||||
sound=False)
|
sound=False)
|
||||||
# Start monitoring kodi events
|
# 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
|
# Start the Websocket Client
|
||||||
if not self.websocket_running:
|
if not self.ws_running:
|
||||||
self.websocket_running = True
|
self.ws_running = True
|
||||||
ws.start()
|
self.ws.start()
|
||||||
# Start the syncing thread
|
# Start the syncing thread
|
||||||
if not self.library_running:
|
if not self.library_running:
|
||||||
self.library_running = True
|
self.library_running = True
|
||||||
library.start()
|
self.library.start()
|
||||||
# Start the Plex Companion thread
|
# Start the Plex Companion thread
|
||||||
if not self.plexCompanion_running:
|
if not self.plexCompanion_running:
|
||||||
self.plexCompanion_running = True
|
self.plexCompanion_running = True
|
||||||
plexCompanion = PlexCompanion.PlexCompanion()
|
self.plexCompanion.start()
|
||||||
plexCompanion.start()
|
if not self.playback_starter_running:
|
||||||
|
self.playback_starter_running = True
|
||||||
|
self.playback_starter.start()
|
||||||
else:
|
else:
|
||||||
if (user.currUser is None) and self.warn_auth:
|
if (self.user.currUser is None) and self.warn_auth:
|
||||||
# Alert user is not authenticated and suppress future warning
|
# Alert user is not authenticated and suppress future
|
||||||
|
# warning
|
||||||
self.warn_auth = False
|
self.warn_auth = False
|
||||||
log.warn("Not authenticated yet.")
|
log.warn("Not authenticated yet.")
|
||||||
|
|
||||||
# User access is restricted.
|
# User access is restricted.
|
||||||
# Keep verifying until access is granted
|
# Keep verifying until access is granted
|
||||||
# unless server goes offline or Kodi is shut down.
|
# 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
|
# Verify access with an API call
|
||||||
user.hasAccess()
|
self.user.hasAccess()
|
||||||
|
|
||||||
if window('plex_online') != "true":
|
if window('plex_online') != "true":
|
||||||
# Server went offline
|
# Server went offline
|
||||||
|
@ -211,7 +235,7 @@ class Service():
|
||||||
# Wait until Plex server is online
|
# Wait until Plex server is online
|
||||||
# or Kodi is shut down.
|
# or Kodi is shut down.
|
||||||
while not monitor.abortRequested():
|
while not monitor.abortRequested():
|
||||||
server = user.getServer()
|
server = self.user.getServer()
|
||||||
if server is False:
|
if server is False:
|
||||||
# No server info set in add-on settings
|
# No server info set in add-on settings
|
||||||
pass
|
pass
|
||||||
|
@ -268,9 +292,9 @@ class Service():
|
||||||
window('suspend_LibraryThread', clear=True)
|
window('suspend_LibraryThread', clear=True)
|
||||||
|
|
||||||
# Start the userclient thread
|
# Start the userclient thread
|
||||||
if not self.userclient_running:
|
if not self.user_running:
|
||||||
self.userclient_running = True
|
self.user_running = True
|
||||||
user.start()
|
self.user.start()
|
||||||
|
|
||||||
break
|
break
|
||||||
|
|
||||||
|
@ -286,27 +310,22 @@ class Service():
|
||||||
|
|
||||||
# Tell all threads to terminate (e.g. several lib sync threads)
|
# Tell all threads to terminate (e.g. several lib sync threads)
|
||||||
window('plex_terminateNow', value='true')
|
window('plex_terminateNow', value='true')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
plexCompanion.stopThread()
|
self.plexCompanion.stopThread()
|
||||||
except:
|
except:
|
||||||
log.warn('plexCompanion already shut down')
|
log.warn('plexCompanion already shut down')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
library.stopThread()
|
self.library.stopThread()
|
||||||
except:
|
except:
|
||||||
log.warn('Library sync already shut down')
|
log.warn('Library sync already shut down')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ws.stopThread()
|
self.ws.stopThread()
|
||||||
except:
|
except:
|
||||||
log.warn('Websocket client already shut down')
|
log.warn('Websocket client already shut down')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
user.stopThread()
|
self.user.stopThread()
|
||||||
except:
|
except:
|
||||||
log.warn('User client already shut down')
|
log.warn('User client already shut down')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
downloadutils.DownloadUtils().stopSession()
|
downloadutils.DownloadUtils().stopSession()
|
||||||
except:
|
except:
|
||||||
|
|
Loading…
Reference in a new issue