Merge branch 'develop'

This commit is contained in:
tomkat83 2017-01-09 21:29:45 +01:00
commit 2836f707c5
37 changed files with 2979 additions and 2077 deletions

View file

@ -1,13 +1,11 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="plugin.video.plexkodiconnect"
name="PlexKodiConnect"
version="1.5.1"
version="1.5.2"
provider-name="croneter">
<requires>
<import addon="xbmc.python" version="2.1.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>
<extension point="xbmc.python.pluginsource"
library="default.py">

View file

@ -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)
- 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

View file

@ -3,36 +3,38 @@
###############################################################################
import logging
import os
import sys
import urlparse
from os import path as os_path
from sys import path as sys_path, argv
from urlparse import parse_qsl
import xbmc
import xbmcaddon
import xbmcgui
from xbmc import translatePath, sleep, executebuiltin
from xbmcaddon import Addon
from xbmcgui import ListItem, Dialog
from xbmcplugin import setResolvedUrl
_addon = xbmcaddon.Addon(id='plugin.video.plexkodiconnect')
_addon = Addon(id='plugin.video.plexkodiconnect')
try:
_addon_path = _addon.getAddonInfo('path').decode('utf-8')
except TypeError:
_addon_path = _addon.getAddonInfo('path').decode()
try:
_base_resource = xbmc.translatePath(os.path.join(
_base_resource = translatePath(os_path.join(
_addon_path,
'resources',
'lib')).decode('utf-8')
except TypeError:
_base_resource = xbmc.translatePath(os.path.join(
_base_resource = translatePath(os_path.join(
_addon_path,
'resources',
'lib')).decode()
sys.path.append(_base_resource)
sys_path.append(_base_resource)
###############################################################################
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():
# MAIN ENTRY POINT
#@utils.profiling()
# @utils.profiling()
def __init__(self):
log.debug("Full sys.argv received: %s" % ARGV)
# Parse parameters
log.warn("Full sys.argv received: %s" % sys.argv)
base_url = sys.argv[0]
params = urlparse.parse_qs(sys.argv[2][1:])
params = dict(parse_qsl(ARGV[2][1:]))
try:
mode = params['mode'][0]
mode = params['mode']
itemid = params.get('id', '')
if itemid:
try:
itemid = itemid[0]
except:
pass
except:
params = {}
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 = {
'reset': utils.reset,
'reset': reset,
'resetauth': entrypoint.resetAuth,
'play': entrypoint.doPlayback,
'passwords': utils.passwordsXML,
'passwords': passwordsXML,
'channels': entrypoint.BrowseChannels,
'channelsfolder': entrypoint.BrowseChannels,
'browsecontent': entrypoint.BrowseContent,
@ -79,7 +94,6 @@ class Main():
'inprogressepisodes': entrypoint.getInProgressEpisodes,
'recentepisodes': entrypoint.getRecentEpisodes,
'refreshplaylist': entrypoint.refreshPlaylist,
'companion': entrypoint.plexCompanion,
'switchuser': entrypoint.switchPlexUser,
'deviceid': entrypoint.resetDeviceId,
'delete': entrypoint.deleteItem,
@ -89,24 +103,24 @@ class Main():
'watchlater': entrypoint.watchlater,
'enterPMS': entrypoint.enterPMS,
'togglePlexTV': entrypoint.togglePlexTV,
'playwatchlater': entrypoint.playWatchLater
'Plex_Node': entrypoint.Plex_Node
}
if "/extrafanart" in sys.argv[0]:
plexpath = sys.argv[2][1:]
plexid = params.get('id', [""])[0]
if "/extrafanart" in ARGV[0]:
plexpath = ARGV[2][1:]
plexid = params.get('id', [""])
entrypoint.getExtraFanArt(plexid, plexpath)
entrypoint.getVideoFiles(plexid, plexpath)
return
if mode == 'fanart':
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
if ("/Extras" in sys.argv[0] or "/VideoFiles" in sys.argv[0] or
"/Extras" in sys.argv[2]):
plexId = params.get('id', [None])[0]
if ("/Extras" in ARGV[0] or "/VideoFiles" in ARGV[0] or
"/Extras" in ARGV[2]):
plexId = params.get('id', None)
entrypoint.getVideoFiles(plexId, params)
if modes.get(mode):
@ -117,59 +131,60 @@ class Main():
modes[mode](itemid, dbid)
elif mode in ("nextup", "inprogressepisodes"):
limit = int(params['limit'][0])
limit = int(params['limit'])
modes[mode](itemid, limit)
elif mode in ("channels","getsubfolders"):
modes[mode](itemid)
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':
modes[mode](
itemid,
params.get('type', [""])[0],
params.get('folderid', [""])[0])
params.get('type', [""]),
params.get('folderid', [""]))
elif mode in ('ondeck', 'recentepisodes'):
modes[mode](
itemid,
params.get('type', [""])[0],
params.get('tagname', [""])[0],
int(params.get('limit', [""])[0]))
params.get('type', [""]),
params.get('tagname', [""]),
int(params.get('limit', [""])))
elif mode == "channelsfolder":
folderid = params['folderid'][0]
folderid = params['folderid']
modes[mode](itemid, folderid)
elif mode == "companion":
modes[mode](itemid, params=sys.argv[2])
elif mode == 'playwatchlater':
modes[mode](params.get('id')[0], params.get('viewOffset')[0])
modes[mode](itemid, params=ARGV[2])
elif mode == 'Plex_Node':
modes[mode](params.get('id'),
params.get('viewOffset'),
params.get('plex_type'))
else:
modes[mode]()
else:
# Other functions
if mode == "settings":
xbmc.executebuiltin('Addon.OpenSettings(plugin.video.plexkodiconnect)')
executebuiltin('Addon.OpenSettings(plugin.video.plexkodiconnect)')
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
xbmcgui.Dialog().ok(
Dialog().ok(
"PlexKodiConnect",
"Unable to run the sync, the add-on is not connected "
"to a Plex server.")
log.error("Not connected to a PMS.")
else:
if mode == 'repair':
utils.window('plex_runLibScan', value="repair")
window('plex_runLibScan', value="repair")
log.info("Requesting repair lib sync")
elif mode == 'manualsync':
log.info("Requesting full library scan")
utils.window('plex_runLibScan', value="full")
window('plex_runLibScan', value="full")
elif mode == "texturecache":
utils.window('plex_runLibScan', value='del_textures')
window('plex_runLibScan', value='del_textures')
else:
entrypoint.doMainListing()

View 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],))

View file

@ -50,8 +50,9 @@ import downloadutils
from utils import window, settings, language as lang, tryDecode, tryEncode, \
DateToKodi, KODILANGUAGE
from PlexFunctions import PLEX_TO_KODI_TIMEFACTOR, PMSHttpsEnabled, \
REMAP_TYPE_FROM_PLEXTYPE
import embydb_functions as embydb
REMAP_TYPE_FROM_PLEXTYPE, PLEX_TYPE_MOVIE, PLEX_TYPE_SHOW, \
PLEX_TYPE_EPISODE
import plexdb_functions as plexdb
###############################################################################
@ -1646,7 +1647,7 @@ class API():
If not found, empty str is returned
"""
return self.item.attrib.get('playQueueItemID', '')
return self.item.attrib.get('playQueueItemID')
def getDataFromPartOrMedia(self, key):
"""
@ -1915,9 +1916,9 @@ class API():
# Return the saved Plex id's, if applicable
# Always seek collection's ids since not provided by PMS
if collection is False:
if media_type == 'movie':
if media_type == PLEX_TYPE_MOVIE:
mediaId = self.getProvider('imdb')
elif media_type == 'show':
elif media_type == PLEX_TYPE_SHOW:
mediaId = self.getProvider('tvdb')
if mediaId is not None:
return mediaId
@ -1927,7 +1928,7 @@ class API():
log.info('Start movie set/collection lookup on themoviedb')
apiKey = settings('themoviedbAPIKey')
if media_type == 'show':
if media_type == PLEX_TYPE_SHOW:
media_type = 'tv'
title = item.get('title', '')
# if the title has the year in remove it as tmdb cannot deal with it...
@ -2305,10 +2306,10 @@ class API():
kodiindex = 0
for stream in mediastreams:
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.
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:
# Direct stream
url = ("%s%s" % (self.server, key))
@ -2318,7 +2319,7 @@ class API():
externalsubs.append(url)
kodiindex += 1
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)
return externalsubs
@ -2393,7 +2394,7 @@ class API():
# listItem.setProperty('isPlayable', 'true')
# listItem.setProperty('isFolder', 'true')
# Further stuff
listItem.setIconImage('DefaultPicture.png')
listItem.setArt({'icon': 'DefaultPicture.png'})
return listItem
def _createVideoListItem(self,
@ -2456,21 +2457,21 @@ class API():
"s%.2de%.2d" % (season, episode))
if appendSxxExx is True:
title = "S%.2dE%.2d - %s" % (season, episode, title)
listItem.setIconImage('DefaultTVShows.png')
listItem.setArt({'icon': 'DefaultTVShows.png'})
if appendShowTitle is True:
title = "%s - %s " % (show, title)
elif typus == "movie":
listItem.setIconImage('DefaultMovies.png')
listItem.setArt({'icon': 'DefaultMovies.png'})
else:
# E.g. clips, trailers, ...
listItem.setIconImage('DefaultVideo.png')
listItem.setArt({'icon': 'DefaultVideo.png'})
plexId = self.getRatingKey()
listItem.setProperty('plexid', plexId)
with embydb.GetEmbyDB() as emby_db:
with plexdb.Get_Plex_DB() as plex_db:
try:
listItem.setProperty('dbid',
str(emby_db.getItem_byId(plexId)[0]))
str(plex_db.getItem_byId(plexId)[0]))
except TypeError:
pass
# Expensive operation
@ -2563,3 +2564,68 @@ class API():
line1=lang(39031) + url,
line2=lang(39032))
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)

View file

@ -1,19 +1,17 @@
# -*- coding: utf-8 -*-
import logging
import threading
import traceback
import socket
from threading import Thread
import Queue
from socket import SHUT_RDWR
import xbmc
from xbmc import sleep
from utils import settings, ThreadMethodsAdditionalSuspend, ThreadMethods
from plexbmchelper import listener, plexgdm, subscribers, functions, \
httppersist, plexsettings
from PlexFunctions import ParseContainerKey, GetPlayQueue, \
ConvertPlexToKodiTime
import playlist
from PlexFunctions import ParseContainerKey, KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE
import player
from entrypoint import Plex_Node
###############################################################################
@ -24,24 +22,23 @@ log = logging.getLogger("PLEX."+__name__)
@ThreadMethodsAdditionalSuspend('plex_serverStatus')
@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 ##===----")
if callback is not None:
self.mgr = callback
self.settings = plexsettings.getSettings()
# Start GDM for server/client discovery
self.client = plexgdm.plexgdm()
self.client.clientDetails(self.settings)
log.debug("Registration string is: %s "
% self.client.getClientDetails())
# Initialize playlist/queue stuff
self.playlist = playlist.Playlist('video')
# kodi player instance
self.player = player.Player()
threading.Thread.__init__(self)
Thread.__init__(self)
def _getStartItem(self, string):
"""
@ -62,62 +59,48 @@ class PlexCompanion(threading.Thread):
def processTasks(self, task):
"""
Processes tasks picked up e.g. by Companion listener
task = {
'action': 'playlist'
'data': as received from Plex companion
}
Processes tasks picked up e.g. by Companion listener, e.g.
{'action': 'playlist',
'data': {'address': 'xyz.plex.direct',
'commandID': '7',
'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)
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:
_, queueId, query = ParseContainerKey(data['containerKey'])
_, ID, query = ParseContainerKey(data['containerKey'])
except Exception as e:
log.error('Exception while processing: %s' % e)
import traceback
log.error("Traceback:\n%s" % traceback.format_exc())
return
if self.playlist is not None:
if self.playlist.Typus() != data.get('type'):
log.debug('Switching to Kodi playlist of type %s'
% data.get('type'))
self.playlist = None
if self.playlist is None:
if data.get('type') == 'music':
self.playlist = playlist.Playlist('music')
else:
self.playlist = playlist.Playlist('video')
if queueId != self.playlist.QueueId():
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!')
playqueue = self.mgr.playqueue.get_playqueue_from_type(
KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[data['type']])
self.mgr.playqueue.update_playqueue_from_PMS(
playqueue,
ID,
repeat=query.get('repeat'),
offset=data.get('offset'))
def run(self):
httpd = False
@ -130,7 +113,7 @@ class PlexCompanion(threading.Thread):
requestMgr = httppersist.RequestMgr()
jsonClass = functions.jsonClass(requestMgr, self.settings)
subscriptionManager = subscribers.SubscriptionManager(
jsonClass, requestMgr, self.player, self.playlist)
jsonClass, requestMgr, self.player, self.mgr)
queue = Queue.Queue(maxsize=100)
@ -151,9 +134,10 @@ class PlexCompanion(threading.Thread):
break
except:
log.error("Unable to start PlexCompanion. Traceback:")
import traceback
log.error(traceback.print_exc())
xbmc.sleep(3000)
sleep(3000)
if start_count == 3:
log.error("Error: Unable to start web helper.")
@ -168,7 +152,7 @@ class PlexCompanion(threading.Thread):
message_count = 0
if httpd:
t = threading.Thread(target=httpd.handle_request)
t = Thread(target=httpd.handle_request)
while not threadStopped():
# If we are not authorized, sleep
@ -177,13 +161,13 @@ class PlexCompanion(threading.Thread):
while threadSuspended():
if threadStopped():
break
xbmc.sleep(1000)
sleep(1000)
try:
message_count += 1
if httpd:
if not t.isAlive():
# Use threads cause the method will stall
t = threading.Thread(target=httpd.handle_request)
t = Thread(target=httpd.handle_request)
t.start()
if message_count == 3000:
@ -202,6 +186,7 @@ class PlexCompanion(threading.Thread):
message_count = 0
except:
log.warn("Error in loop, continuing anyway. Traceback:")
import traceback
log.warn(traceback.format_exc())
# See if there's anything we need to process
try:
@ -214,12 +199,12 @@ class PlexCompanion(threading.Thread):
queue.task_done()
# Don't sleep
continue
xbmc.sleep(20)
sleep(20)
client.stop_all()
if httpd:
try:
httpd.socket.shutdown(socket.SHUT_RDWR)
httpd.socket.shutdown(SHUT_RDWR)
except:
pass
finally:

View file

@ -20,46 +20,118 @@ addonName = 'PlexKodiConnect'
# Multiply Plex time by this factor to receive Kodi time
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 = (
'video',
'movie',
'set',
'tvshow',
'season',
'episode',
'musicvideo'
KODI_TYPE_VIDEO,
KODI_TYPE_MOVIE,
KODI_TYPE_SHOW,
KODI_TYPE_SEASON,
KODI_TYPE_EPISODE,
KODI_TYPE_SET
)
# Possible output of Kodi's ListItem.DBTYPE for all audio items
KODI_AUDIOTYPES = (
'music',
'song',
'album',
'artist'
KODI_TYPE_SONG,
KODI_TYPE_ALBUM,
KODI_TYPE_ARTIST,
)
ITEMTYPE_FROM_PLEXTYPE = {
'movie': 'Movies',
'season': 'TVShows',
'episode': 'TVShows',
'show': 'TVShows',
'artist': 'Music',
'album': 'Music',
'track': 'Music',
'song': 'Music'
PLEX_TYPE_MOVIE: 'Movies',
PLEX_TYPE_SEASON: 'TVShows',
KODI_TYPE_EPISODE: 'TVShows',
PLEX_TYPE_SHOW: 'TVShows',
PLEX_TYPE_ARTIST: 'Music',
PLEX_TYPE_ALBUM: 'Music',
PLEX_TYPE_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 = {
'movie': 'movie',
'episode': 'episode',
'track': 'song',
'artist': 'artist',
'album': 'album',
PLEX_TYPE_MOVIE: KODI_TYPE_MOVIE,
PLEX_TYPE_EPISODE: KODI_TYPE_EPISODE,
PLEX_TYPE_SEASON: KODI_TYPE_SEASON,
PLEX_TYPE_SHOW: KODI_TYPE_SHOW,
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',
'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 = {
'movie': 'movie',
@ -159,22 +231,6 @@ def SelectStreams(url, args):
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):
"""
Returns raw API metadata for key as an etree XML.
@ -388,23 +444,22 @@ def GetPlexCollections(mediatype):
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.
"""
trailerNumber = settings('trailerNumber')
if not trailerNumber:
trailerNumber = '3'
url = "{server}/playQueues"
args = {
'type': mediatype,
'uri': ('library://' + librarySectionUUID +
'/item/%2Flibrary%2Fmetadata%2F' + itemid),
'includeChapters': '1',
'extrasPrefixCount': trailerNumber,
'shuffle': '0',
'repeat': '0'
}
if trailers is True:
args['extrasPrefixCount'] = settings('trailerNumber')
xml = downloadutils.DownloadUtils().downloadUrl(
url + '?' + urlencode(args), action_type="POST")
try:

View file

@ -281,7 +281,7 @@ class Artwork():
def cacheTexture(self, url):
# Cache a single image url to the texture cache
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):
# Kodi conversion table

View file

@ -49,7 +49,7 @@ class ClientInfo():
'X-Plex-Product': self.getAddonName(),
'X-Plex-Version': self.getVersion(),
'X-Plex-Client-Identifier': self.getDeviceId(),
'X-Plex-Provides': 'player',
'X-Plex-Provides': 'client,controller,player',
}
if window('pms_token'):

View file

@ -8,7 +8,7 @@ import xbmc
import xbmcaddon
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 dialogs import context
@ -75,8 +75,8 @@ class ContextMenu(object):
def _get_item_id(cls, kodi_id, item_type):
item_id = xbmc.getInfoLabel('ListItem.Property(plexid)')
if not item_id and kodi_id and item_type:
with embydb.GetEmbyDB() as emby_db:
item = emby_db.getItem_byKodiId(kodi_id, item_type)
with plexdb.Get_Plex_DB() as plexcursor:
item = plexcursor.getItem_byKodiId(kodi_id, item_type)
try:
item_id = item[0]
except TypeError:
@ -140,8 +140,8 @@ class ContextMenu(object):
elif selected == OPTIONS['PMS_Play']:
self._PMS_play()
elif selected == OPTIONS['Refresh']:
self.emby.refreshItem(self.item_id)
# elif selected == OPTIONS['Refresh']:
# self.emby.refreshItem(self.item_id)
# elif selected == OPTIONS['AddFav']:
# self.emby.updateUserRating(self.item_id, favourite=True)
@ -212,6 +212,6 @@ class ContextMenu(object):
'mode': "play"
}
from urllib import urlencode
handle = ("plugin://plugin.video.plexkodiconnect.movies?%s"
handle = ("plugin://plugin.video.plexkodiconnect/movies?%s"
% urlencode(params))
xbmc.executebuiltin('RunPlugin(%s)' % handle)

View file

@ -67,7 +67,7 @@ class UsersConnect(xbmcgui.WindowXMLDialog):
if self.kodi_version > 15:
item.setArt({'Icon': user_image})
else:
item.setIconImage(user_image)
item.setArt({'icon': user_image})
return item

View file

@ -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

View file

@ -17,12 +17,13 @@ from utils import window, settings, language as lang
from utils import tryDecode, tryEncode, CatchExceptions
import clientinfo
import downloadutils
import embydb_functions as embydb
import plexdb_functions as plexdb
import playbackutils as pbutils
import playlist
import PlexFunctions
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():
"""
Lets user choose from list of PMS
@ -130,45 +97,21 @@ def togglePlexTV():
sound=False)
def PassPlaylist(xml, resume=None):
"""
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):
def Plex_Node(url, viewOffset, plex_type, playdirectly=False):
"""
Called only for a SINGLE element for Plex.tv watch later
Always to return with a "setResolvedUrl"
"""
log.info('playWatchLater called with id: %s, viewOffset: %s'
% (itemid, viewOffset))
log.info('Plex_Node called with url: %s, viewOffset: %s'
% (url, viewOffset))
# Plex redirect, e.g. watch later. Need to get actual URLs
xml = downloadutils.DownloadUtils().downloadUrl(itemid,
authenticate=False)
if xml in (None, 401):
log.error("Could not resolve url %s" % itemid)
return xbmcplugin.setResolvedUrl(
int(sys.argv[1]), False, xbmcgui.ListItem())
xml = downloadutils.DownloadUtils().downloadUrl(url)
try:
xml[0].attrib
except:
log.error('Could not download PMS metadata')
return
if viewOffset != '0':
try:
viewOffset = int(PlexFunctions.PLEX_TO_KODI_TIMEFACTOR *
@ -178,41 +121,20 @@ def playWatchLater(itemid, viewOffset):
else:
window('plex_customplaylist.seektime', value=str(viewOffset))
log.info('Set resume point to %s' % str(viewOffset))
return pbutils.PlaybackUtils(xml).play(None, 'plexnode')
def doPlayback(itemid, dbid):
"""
Called only for a SINGLE element, not playQueues
Always to return with a "setResolvedUrl"
"""
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)
typus = PlexFunctions.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[plex_type]
playqueue = Playqueue().get_playqueue_from_type(typus)
result = pbutils.PlaybackUtils(xml, playqueue).play(
None,
kodi_id='plexnode',
plex_lib_UUID=xml.attrib.get('librarySectionUUID'))
if result.listitem:
listitem = convert_PKC_to_listitem(result.listitem)
else:
# Video
return pbutils.PlaybackUtils(xml).play(itemid, dbid)
return
if playdirectly:
xbmc.Player().play(listitem.getfilename(), listitem)
else:
xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem)
##### DO RESET AUTH #####
@ -319,12 +241,8 @@ def deleteItem():
log.error("Unknown type, unable to proceed.")
return
from utils import kodiSQL
embyconn = kodiSQL('emby')
embycursor = embyconn.cursor()
emby_db = embydb.Embydb_Functions(embycursor)
item = emby_db.getItem_byKodiId(dbid, itemtype)
embycursor.close()
with plexdb.Get_Plex_DB() as plexcursor:
item = plexcursor.getItem_byKodiId(dbid, itemtype)
try:
plexid = item[0]
@ -467,99 +385,6 @@ def BrowseContent(viewname, browse_type="", folderid=""):
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 #####
def BrowseChannels(itemid, folderid=None):
@ -664,7 +489,7 @@ def createListItem(item, appendShowTitle=False, appendSxxExx=False):
li.setProperty('totaltime', str(item['resume']['total']))
li.setArt(item['art'])
li.setThumbnailImage(item['art'].get('thumb',''))
li.setIconImage('DefaultTVShows.png')
li.setArt({'icon': 'DefaultTVShows.png'})
li.setProperty('dbid', str(item['episodeid']))
li.setProperty('fanart_image', item['art'].get('tvshow.fanart',''))
for key, value in item['streamdetails'].iteritems():
@ -1095,14 +920,14 @@ def BrowsePlexContent(viewid, mediatype="", folderid=""):
li.setProperty('IsPlayable', 'false')
path = "%s?id=%s&mode=browseplex&type=%s&folderid=%s" \
% (sys.argv[0], viewid, mediatype, API.getKey())
pbutils.PlaybackUtils(item).setArtwork(li)
API.set_listitem_artwork(li)
xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]),
url=path,
listitem=li,
isFolder=True)
else:
li = API.CreateListItemFromPlexItem()
pbutils.PlaybackUtils(item).setArtwork(li)
API.set_listitem_artwork(li)
xbmcplugin.addDirectoryItem(
handle=int(sys.argv[1]),
url=li.getProperty("path"),
@ -1159,7 +984,7 @@ def getOnDeck(viewid, mediatype, tagname, limit):
appendShowTitle=appendShowTitle,
appendSxxExx=appendSxxExx)
API.AddStreamInfo(listitem)
pbutils.PlaybackUtils(item).setArtwork(listitem)
API.set_listitem_artwork(listitem)
if directpaths:
url = API.getFilePath()
else:
@ -1168,7 +993,7 @@ def getOnDeck(viewid, mediatype, tagname, limit):
'id': API.getRatingKey(),
'dbid': listitem.getProperty('dbid')
}
url = "plugin://plugin.video.plexkodiconnect.tvshows/?%s" \
url = "plugin://plugin.video.plexkodiconnect/tvshows/?%s" \
% urllib.urlencode(params)
xbmcplugin.addDirectoryItem(
handle=int(sys.argv[1]),
@ -1306,15 +1131,16 @@ def watchlater():
xbmcplugin.setContent(int(sys.argv[1]), 'movies')
url = "plugin://plugin.video.plexkodiconnect/"
params = {
'mode': "playwatchlater",
'mode': "Plex_Node",
}
for item in xml:
API = PlexAPI.API(item)
listitem = API.CreateListItemFromPlexItem()
API.AddStreamInfo(listitem)
pbutils.PlaybackUtils(item).setArtwork(listitem)
API.set_listitem_artwork(listitem)
params['id'] = item.attrib.get('key')
params['viewOffset'] = item.attrib.get('viewOffset', '0')
params['plex_type'] = item.attrib.get('type')
xbmcplugin.addDirectoryItem(
handle=int(sys.argv[1]),
url="%s?%s" % (url, urllib.urlencode(params)),

View file

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

File diff suppressed because it is too large Load diff

View file

@ -7,6 +7,7 @@ from ntpath import dirname
import artwork
from utils import kodiSQL, KODIVERSION
from PlexFunctions import KODI_TYPE_MOVIE, KODI_TYPE_EPISODE
###############################################################################
@ -31,8 +32,8 @@ class GetKodiDB():
def __enter__(self):
self.kodiconn = kodiSQL(self.itemType)
self.emby_db = Kodidb_Functions(self.kodiconn.cursor())
return self.emby_db
kodi_db = Kodidb_Functions(self.kodiconn.cursor())
return kodi_db
def __exit__(self, type, value, traceback):
self.kodiconn.commit()
@ -61,7 +62,7 @@ class Kodidb_Functions():
self.cursor.execute(
query, ('movies',
'metadata.local',
'plugin://plugin.video.plexkodiconnect.movies%%'))
'plugin://plugin.video.plexkodiconnect/movies%%'))
def getParentPathId(self, path):
"""
@ -869,7 +870,7 @@ class Kodidb_Functions():
self.cursor.execute(query, (idFile,))
try:
itemId = self.cursor.fetchone()[0]
typus = 'movie'
typus = KODI_TYPE_MOVIE
except TypeError:
# Try tv shows next
query = ' '.join((
@ -880,7 +881,7 @@ class Kodidb_Functions():
self.cursor.execute(query, (idFile,))
try:
itemId = self.cursor.fetchone()[0]
typus = 'episode'
typus = KODI_TYPE_EPISODE
except TypeError:
log.warn('Unexpectantly did not find a match!')
return
@ -907,13 +908,13 @@ class Kodidb_Functions():
return ids
def getVideoRuntime(self, kodiid, mediatype):
if mediatype == 'movie':
if mediatype == KODI_TYPE_MOVIE:
query = ' '.join((
"SELECT c11",
"FROM movie",
"WHERE idMovie = ?",
))
elif mediatype == 'episode':
elif mediatype == KODI_TYPE_EPISODE:
query = ' '.join((
"SELECT c09",
"FROM episode",
@ -1397,3 +1398,89 @@ class Kodidb_Functions():
query = "INSERT OR REPLACE INTO song_genre(idGenre, idSong) values(?, ?)"
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)

View file

@ -3,17 +3,16 @@
###############################################################################
import logging
import json
from json import loads
import xbmc
import xbmcgui
from xbmc import Monitor, Player, sleep
import downloadutils
import embydb_functions as embydb
import kodidb_functions as kodidb
import playbackutils as pbutils
import plexdb_functions as plexdb
from utils import window, settings, CatchExceptions, tryDecode, tryEncode
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):
def __init__(self):
class KodiMonitor(Monitor):
def __init__(self, callback):
self.mgr = callback
self.doUtils = downloadutils.DownloadUtils().downloadUrl
self.xbmcplayer = xbmc.Player()
xbmc.Monitor.__init__(self)
self.xbmcplayer = Player()
self.playqueue = self.mgr.playqueue
Monitor.__init__(self)
log.info("Kodi monitor started.")
def onScanStarted(self, library):
@ -70,7 +70,7 @@ class KodiMonitor(xbmc.Monitor):
def onNotification(self, sender, method, data):
if data:
data = json.loads(data, 'utf-8')
data = loads(data, 'utf-8')
log.debug("Method: %s Data: %s" % (method, data))
if method == "Player.OnPlay":
@ -92,18 +92,18 @@ class KodiMonitor(xbmc.Monitor):
log.info("Item is invalid for playstate update.")
else:
# Send notification to the server.
with embydb.GetEmbyDB() as emby_db:
emby_dbitem = emby_db.getItem_byKodiId(kodiid, item_type)
with plexdb.Get_Plex_DB() as plexcur:
plex_dbitem = plexcur.getItem_byKodiId(kodiid, item_type)
try:
itemid = emby_dbitem[0]
itemid = plex_dbitem[0]
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")
else:
# 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
window('emby_skipWatched%s' % itemid, clear=True)
window('plex_skipWatched%s' % itemid, clear=True)
else:
# notify the server
if playcount != 0:
@ -112,40 +112,7 @@ class KodiMonitor(xbmc.Monitor):
scrobble(itemid, 'unwatched')
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
'''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":
# Connection is going to sleep
@ -154,18 +121,15 @@ class KodiMonitor(xbmc.Monitor):
elif method == "System.OnWake":
# Allow network to wake up
xbmc.sleep(10000)
sleep(10000)
window('plex_onWake', value="true")
window('plex_online', value="false")
elif method == "GUI.OnScreensaverDeactivated":
if settings('dbSyncScreensaver') == "true":
xbmc.sleep(5000)
sleep(5000)
window('plex_runLibScan', value="full")
elif method == "Playlist.OnClear":
pass
def PlayBackStart(self, data):
"""
Called whenever a playback is started
@ -177,7 +141,7 @@ class KodiMonitor(xbmc.Monitor):
currentFile = None
count = 0
while currentFile is None:
xbmc.sleep(100)
sleep(100)
try:
currentFile = self.xbmcplayer.getPlayingFile()
except:
@ -201,7 +165,7 @@ class KodiMonitor(xbmc.Monitor):
# Try to get a Kodi ID
# 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
plexid = None if not plexid else plexid
kodiid = None
@ -215,27 +179,16 @@ class KodiMonitor(xbmc.Monitor):
# 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'
and not currentFile.startswith('http')):
try:
filename = currentFile.rsplit('/', 1)[1]
path = currentFile.rsplit('/', 1)[0] + '/'
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
(kodiid, typus) = get_kodiid_from_filename(currentFile)
if kodiid is None:
return
if plexid is None:
# Get Plex' item id
with embydb.GetEmbyDB() as emby_db:
emby_dbitem = emby_db.getItem_byKodiId(kodiid, typus)
with plexdb.Get_Plex_DB() as plexcursor:
plex_dbitem = plexcursor.getItem_byKodiId(kodiid, typus)
try:
plexid = emby_dbitem[0]
plexid = plex_dbitem[0]
except TypeError:
log.info("No Plex id returned for kodiid %s. Aborting playback"
" report" % kodiid)
@ -256,24 +209,25 @@ class KodiMonitor(xbmc.Monitor):
# Save currentFile for cleanup later and to be able to access refs
window('plex_lastPlayedFiled', value=currentFile)
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')
def StartDirectPath(self, plexid, type, currentFile):
"""
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:
result[0].attrib
xml[0].attrib
except:
log.error('Did not receive a valid XML for plexid %s.' % plexid)
return False
# Setup stuff, because playback was started by Kodi, not PKC
pbutils.PlaybackUtils(result[0]).setProperties(
currentFile, xbmcgui.ListItem())
api = API(xml[0])
listitem = api.CreateListItemFromPlexItem()
api.set_playback_win_props(currentFile, listitem)
if type == "song" and settings('streamMusic') == "true":
window('emby_%s.playmethod' % currentFile, value="DirectStream")
window('plex_%s.playmethod' % currentFile, value="DirectStream")
else:
window('emby_%s.playmethod' % currentFile, value="DirectPlay")
window('plex_%s.playmethod' % currentFile, value="DirectPlay")
log.debug('Window properties set for direct paths!')

View file

@ -11,7 +11,7 @@ import xbmc
import xbmcgui
import xbmcvfs
from utils import window, settings, getUnixTimestamp, kodiSQL, sourcesXML,\
from utils import window, settings, getUnixTimestamp, sourcesXML,\
ThreadMethods, ThreadMethodsAdditionalStop, LogTime, getScreensaver,\
setScreensaver, playlistXSP, language as lang, DateToKodi, reset,\
advancedSettingsXML, getKodiVideoDBPath, tryDecode, deletePlaylists,\
@ -19,7 +19,7 @@ from utils import window, settings, getUnixTimestamp, kodiSQL, sourcesXML,\
import clientinfo
import downloadutils
import itemtypes
import embydb_functions as embydb
import plexdb_functions as plexdb
import kodidb_functions as kodidb
import userclient
import videonodes
@ -302,9 +302,9 @@ class ProcessFanartThread(Thread):
# Leave the Plex art untouched
allartworks = None
else:
with embydb.GetEmbyDB() as emby_db:
with plexdb.Get_Plex_DB() as plex_db:
try:
kodiId = emby_db.getItem_byId(item['itemId'])[0]
kodiId = plex_db.getItem_byId(item['itemId'])[0]
except TypeError:
log.error('Could not get Kodi id for plex id %s'
% item['itemId'])
@ -356,19 +356,10 @@ class ProcessFanartThread(Thread):
@ThreadMethods
class LibrarySync(Thread):
"""
librarysync.LibrarySync(queue)
where (communication with websockets)
queue: Queue object for background sync
"""
# Borg, even though it's planned to only have 1 instance up and running!
_shared_state = {}
def __init__(self, callback=None):
self.mgr = callback
def __init__(self, queue):
self.__dict__ = self._shared_state
# Communication with websockets
self.queue = queue
self.itemsToProcess = []
self.sessionKeys = []
self.fanartqueue = Queue.Queue()
@ -455,7 +446,9 @@ class LibrarySync(Thread):
return False
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:
break
for view in sections:
@ -539,24 +532,34 @@ class LibrarySync(Thread):
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')
embycursor = embyconn.cursor()
# Create the tables for the emby database
# emby, view, version
embycursor.execute(
"""CREATE TABLE IF NOT EXISTS emby(
emby_id TEXT UNIQUE, media_folder TEXT, emby_type TEXT, media_type TEXT, kodi_id INTEGER,
kodi_fileid INTEGER, kodi_pathid INTEGER, parent_id INTEGER, checksum INTEGER)""")
embycursor.execute(
"""CREATE TABLE IF NOT EXISTS view(
view_id TEXT UNIQUE, view_name TEXT, media_type TEXT, kodi_tagid INTEGER)""")
embycursor.execute("CREATE TABLE IF NOT EXISTS version(idVersion TEXT)")
embyconn.commit()
# content sync: movies, tvshows, musicvideos, music
embyconn.close()
with plexdb.Get_Plex_DB() as plex_db:
# Create the tables for the plex database
plex_db.plexcursor.execute('''
CREATE TABLE IF NOT EXISTS plex(
plex_id TEXT UNIQUE,
view_id TEXT,
plex_type TEXT,
kodi_type TEXT,
kodi_id INTEGER,
kodi_fileid INTEGER,
kodi_pathid INTEGER,
parent_id INTEGER,
checksum INTEGER)
''')
plex_db.plexcursor.execute('''
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_actor_db_index()
@ -643,12 +646,13 @@ class LibrarySync(Thread):
log.error('Path hack failed with error message: %s' % str(e))
return True
def processView(self, folderItem, kodi_db, emby_db, totalnodes):
def processView(self, folderItem, kodi_db, plex_db, totalnodes):
vnodes = self.vnodes
folder = folderItem.attrib
mediatype = folder['type']
# 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
# Prevent duplicate for nodes of the same type
@ -661,8 +665,8 @@ class LibrarySync(Thread):
foldername = folder['title']
viewtype = folder['type']
# Get current media folders from emby database
view = emby_db.getView_byId(folderid)
# Get current media folders from plex database
view = plex_db.getView_byId(folderid)
try:
current_viewname = view[0]
current_viewtype = view[1]
@ -672,12 +676,12 @@ class LibrarySync(Thread):
tagid = kodi_db.createTag(foldername)
# Create playlist for the video library
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)
playlists.append(foldername)
# Create the video node
if (foldername not in nodes and
mediatype not in ("musicvideos", "artist")):
mediatype != PF.PLEX_TYPE_ARTIST):
vnodes.viewNode(sorted_views.index(foldername),
foldername,
mediatype,
@ -685,8 +689,8 @@ class LibrarySync(Thread):
folderid)
nodes.append(foldername)
totalnodes += 1
# Add view to emby database
emby_db.addView(folderid, foldername, viewtype, tagid)
# Add view to plex database
plex_db.addView(folderid, foldername, viewtype, tagid)
else:
log.info(' '.join((
"Found viewid: %s" % folderid,
@ -708,10 +712,10 @@ class LibrarySync(Thread):
tagid = kodi_db.createTag(foldername)
# Update view with new info
emby_db.updateView(foldername, tagid, folderid)
plex_db.updateView(foldername, tagid, folderid)
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
# no other tags with the same name before deleting
# playlist.
@ -731,7 +735,7 @@ class LibrarySync(Thread):
delete=True)
# Added new playlist
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,
@ -748,16 +752,16 @@ class LibrarySync(Thread):
totalnodes += 1
# Update items with new tag
items = emby_db.getItem_byView(folderid)
items = plex_db.getItem_byView(folderid)
for item in items:
# Remove the "s" from viewtype for tags
kodi_db.updateTag(
current_tagid, tagid, item[0], current_viewtype[:-1])
else:
# Validate the playlist exists or recreate it
if mediatype != "artist":
if mediatype != PF.PLEX_TYPE_ARTIST:
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,
@ -792,22 +796,22 @@ class LibrarySync(Thread):
# For whatever freaking reason, .copy() or dict() does NOT work?!?!?!
self.nodes = {
'movie': [],
'show': [],
'artist': [],
'photo': []
PF.PLEX_TYPE_MOVIE: [],
PF.PLEX_TYPE_SHOW: [],
PF.PLEX_TYPE_ARTIST: [],
PF.PLEX_TYPE_PHOTO: []
}
self.playlists = {
'movie': [],
'show': [],
'artist': [],
'photo': []
PF.PLEX_TYPE_MOVIE: [],
PF.PLEX_TYPE_SHOW: [],
PF.PLEX_TYPE_ARTIST: [],
PF.PLEX_TYPE_PHOTO: []
}
self.sorted_views = []
for view in sections:
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'])
log.debug('Sorted views: %s' % self.sorted_views)
@ -815,15 +819,15 @@ class LibrarySync(Thread):
vnodes.clearProperties()
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
# 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:
for folderItem in sections:
totalnodes = self.processView(folderItem,
kodi_db,
emby_db,
plex_db,
totalnodes)
# Add video nodes listings
# Plex: there seem to be no favorites/favorites tag
@ -842,19 +846,17 @@ class LibrarySync(Thread):
# "movies",
# "channels")
# totalnodes += 1
with kodidb.GetKodiDB('music') as kodi_db:
pass
# Save total
window('Plex.nodes.total', str(totalnodes))
# 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)
for view in self.old_views:
emby_db.removeView(view)
plex_db.removeView(view)
# update views for all:
self.views = emby_db.getAllViewInfo()
self.views = plex_db.getAllViewInfo()
log.info("Finished processing views. Views saved: %s" % self.views)
return True
@ -1038,9 +1040,10 @@ class LibrarySync(Thread):
if (settings('FanartTV') == 'true' and
itemType in ('Movies', 'TVShows')):
# 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:
if item['mediaType'] in ('movie', 'show'):
if item['mediaType'] in (PF.KODI_TYPE_MOVIE, PF.KODI_TYPE_SHOW):
self.fanartqueue.put({
'itemId': item['itemId'],
'class': itemType,
@ -1056,16 +1059,17 @@ class LibrarySync(Thread):
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))
self.allKodiElementsId = {}
if self.compare:
with embydb.GetEmbyDB() as emby_db:
with plexdb.Get_Plex_DB() as plex_db:
# Get movies from Plex server
# Pull the list of movies and boxsets in Kodi
try:
self.allKodiElementsId = dict(emby_db.getChecksum('Movie'))
self.allKodiElementsId = dict(
plex_db.getChecksum(PF.PLEX_TYPE_MOVIE))
except ValueError:
self.allKodiElementsId = {}
@ -1148,11 +1152,13 @@ class LibrarySync(Thread):
self.allKodiElementsId = {}
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
for kind in ('Series', 'Season', 'Episode'):
for kind in (PF.PLEX_TYPE_SHOW,
PF.PLEX_TYPE_SEASON,
PF.PLEX_TYPE_EPISODE):
try:
elements = dict(emby_db.getChecksum(kind))
elements = dict(plex.getChecksum(kind))
self.allKodiElementsId.update(elements)
# Yet empty/not yet synched
except ValueError:
@ -1270,22 +1276,24 @@ class LibrarySync(Thread):
def PlexMusic(self):
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))
methods = {
'MusicArtist': 'add_updateArtist',
'MusicAlbum': 'add_updateAlbum',
'Audio': 'add_updateSong'
PF.PLEX_TYPE_ARTIST: 'add_updateArtist',
PF.PLEX_TYPE_ALBUM: 'add_updateAlbum',
PF.PLEX_TYPE_SONG: 'add_updateSong'
}
urlArgs = {
'MusicArtist': {'type': 8},
'MusicAlbum': {'type': 9},
'Audio': {'type': 10}
PF.PLEX_TYPE_ARTIST: {'type': 8},
PF.PLEX_TYPE_ALBUM: {'type': 9},
PF.PLEX_TYPE_SONG: {'type': 10}
}
# 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():
return False
log.debug("Start processing music %s" % kind)
@ -1318,10 +1326,10 @@ class LibrarySync(Thread):
# Get a list of items already existing in Kodi db
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
try:
elements = dict(emby_db.getChecksum(kind))
elements = dict(plex_db.getChecksum(kind))
self.allKodiElementsId.update(elements)
# Yet empty/nothing yet synched
except ValueError:
@ -1569,14 +1577,14 @@ class LibrarySync(Thread):
where
"""
items = []
with embydb.GetEmbyDB() as emby_db:
with plexdb.Get_Plex_DB() as plex_db:
for item in data:
# Drop buffering messages immediately
state = item.get('state')
if state == 'buffering':
continue
ratingKey = item.get('ratingKey')
kodiInfo = emby_db.getItem_byId(ratingKey)
kodiInfo = plex_db.getItem_byId(ratingKey)
if kodiInfo is None:
# Item not (yet) in Kodi library
continue
@ -1663,7 +1671,7 @@ class LibrarySync(Thread):
# Now tell Kodi where we are
for item in items:
itemFkt = getattr(itemtypes,
PF.ITEMTYPE_FROM_PLEXTYPE[item['kodi_type']])
PF.ITEMTYPE_FROM_KODITYPE[item['kodi_type']])
with itemFkt() as Fkt:
Fkt.updatePlaystate(item)
@ -1675,12 +1683,12 @@ class LibrarySync(Thread):
"""
items = []
typus = {
'Movie': 'Movies',
'Series': 'TVShows'
PF.PLEX_TYPE_MOVIE: 'Movies',
PF.PLEX_TYPE_SHOW: 'TVShows'
}
with embydb.GetEmbyDB() as emby_db:
with plexdb.Get_Plex_DB() as plex_db:
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(items)
for item in items:
@ -1720,7 +1728,8 @@ class LibrarySync(Thread):
xbmcplayer = xbmc.Player()
queue = self.queue
# Link to Websocket queue
queue = self.mgr.ws.queue
startupComplete = False
self.views = []

View file

@ -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
View 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

View 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 ##===----")

View file

@ -3,20 +3,25 @@
###############################################################################
import logging
import sys
from urllib import urlencode
from threading import Thread
import xbmc
from xbmc import getCondVisibility, Player
import xbmcgui
import xbmcplugin
import playutils as putils
import playlist
from utils import window, settings, tryEncode, tryDecode
import downloadutils
import PlexAPI
import PlexFunctions as PF
from PlexAPI import API
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():
def __init__(self, item):
def __init__(self, xml, playqueue):
self.xml = xml
self.playqueue = playqueue
self.item = item
self.API = PlexAPI.API(item)
self.userid = window('currUserId')
self.server = window('pms_server')
if self.API.getType() == 'track':
self.pl = playlist.Playlist(typus='music')
else:
self.pl = playlist.Playlist(typus='video')
def play(self, itemid, dbid=None):
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.")
def play(self, plex_id, kodi_id=None, plex_lib_UUID=None):
"""
plex_lib_UUID: xml attribute 'librarySectionUUID', needed for posting
to the PMS
"""
log.info("Playbackutils called")
item = self.xml[0]
api = API(item)
playqueue = self.playqueue
xml = None
result = Playback_Successful()
listitem = ListItem()
playutils = putils.PlayUtils(item)
playurl = playutils.getPlayUrl()
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'):
# Item is not in Kodi database, is a trailer or plex redirect
if kodi_id in (None, 'plextrailer', 'plexnode'):
# Item is not in Kodi database, is a trailer/clip or plex redirect
# e.g. plex.tv watch later
API.CreateListItemFromPlexItem(listitem)
self.setArtwork(listitem)
if dbid == 'plexnode':
api.CreateListItemFromPlexItem(listitem)
api.set_listitem_artwork(listitem)
if kodi_id == 'plexnode':
# 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(
'{server}%s' % item[0][0][0].attrib.get('key'))
if xml in (None, 401):
'{server}%s' % item[0][0].attrib.get('key'))
try:
xml[0].attrib
except (TypeError, AttributeError):
log.error('Could not download %s'
% item[0][0][0].attrib.get('key'))
return xbmcplugin.setResolvedUrl(
int(sys.argv[1]), False, listitem)
% item[0][0].attrib.get('key'))
return
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":
window('emby_%s.playmethod' % playurl, clear=True)
window('plex_%s.playmethod' % playurl, clear=True)
playurl = tryEncode(playutils.audioSubsPref(
listitem, tryDecode(playurl)))
window('emby_%s.playmethod' % playurl, "Transcode")
window('plex_%s.playmethod' % playurl, "Transcode")
listitem.setPath(playurl)
self.setProperties(playurl, listitem)
return xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem)
api.set_playback_win_props(playurl, 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'
window('plex_contextplay', clear=True)
homeScreen = xbmc.getCondVisibility('Window.IsActive(home)')
kodiPl = self.pl.playlist
sizePlaylist = kodiPl.size()
homeScreen = getCondVisibility('Window.IsActive(home)')
sizePlaylist = len(playqueue.items)
if contextmenu_play:
# Need to start with the items we're inserting here
startPos = sizePlaylist
else:
# Can return -1
startPos = max(kodiPl.getposition(), 0)
startPos = max(playqueue.kodi_pl.getposition(), 0)
self.currentPosition = startPos
propertiesPlayback = window('plex_playbackProps') == "true"
@ -108,72 +111,106 @@ class PlaybackUtils():
log.info("Playlist plugin position: %s" % self.currentPosition)
log.info("Playlist size: %s" % sizePlaylist)
############### RESUME POINT ################
seektime, runtime = API.getRuntime()
# RESUME POINT ################
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.
# Otherwise we get a loop.
if not propertiesPlayback:
window('plex_playbackProps', value="true")
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
not contextmenu_play):
# Need to add a dummy file because the first item will fail
log.debug("Adding dummy file to playlist.")
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
self.pl.removefromPlaylist(startPos+1)
# Readd the original item to playlist - via jsonrpc so we have full metadata
self.pl.insertintoPlaylist(
remove_from_Kodi_playlist(
playqueue,
startPos+1)
# Readd the original item to playlist - via jsonrpc so we have
# full metadata
add_item_to_kodi_playlist(
playqueue,
self.currentPosition+1,
dbid,
PF.KODITYPE_FROM_PLEXTYPE[API.getType()])
kodi_id=kodi_id,
kodi_type=kodi_type,
file=playurl)
self.currentPosition += 1
############### -- CHECK FOR INTROS ################
if (settings('enableCinema') == "true" and not seektime):
# if we have any play them when the movie/show is not being resumed
xml = PF.GetPlexPlaylist(
itemid,
item.attrib.get('librarySectionUUID'),
mediatype=API.getType())
introsPlaylist = self.AddTrailers(xml)
############### -- ADD MAIN ITEM ONLY FOR HOMESCREEN ##############
# -- ADD TRAILERS ################
if trailers:
for i, item in enumerate(xml):
if i == len(xml) - 1:
# Don't add the main movie itself
break
self.add_trailer(item)
introsPlaylist = True
# -- ADD MAIN ITEM ONLY FOR HOMESCREEN ##############
if homeScreen and not seektime and not sizePlaylist:
# Extend our current playlist with the actual item to play
# only if there's no playlist first
log.info("Adding main item to playlist.")
self.pl.addtoPlaylist(
dbid,
PF.KODITYPE_FROM_PLEXTYPE[API.getType()])
add_item_to_kodi_playlist(
playqueue,
self.currentPosition,
kodi_id,
kodi_type)
elif contextmenu_play:
if window('useDirectPaths') == 'true':
# Cannot add via JSON with full metadata because then we
# Would be using the direct path
log.debug("Adding contextmenu item for direct paths")
if window('emby_%s.playmethod' % playurl) == "Transcode":
window('emby_%s.playmethod' % playurl,
if window('plex_%s.playmethod' % playurl) == "Transcode":
window('plex_%s.playmethod' % playurl,
clear=True)
playurl = tryEncode(playutils.audioSubsPref(
listitem, tryDecode(playurl)))
window('emby_%s.playmethod' % playurl,
window('plex_%s.playmethod' % playurl,
value="Transcode")
self.setProperties(playurl, listitem)
self.setArtwork(listitem)
API.CreateListItemFromPlexItem(listitem)
api.CreateListItemFromPlexItem(listitem)
api.set_playback_win_props(playurl, listitem)
api.set_listitem_artwork(listitem)
kodiPl.add(playurl, listitem, index=self.currentPosition+1)
else:
# Full metadata
self.pl.insertintoPlaylist(
self.currentPosition+1,
dbid,
PF.KODITYPE_FROM_PLEXTYPE[API.getType()])
kodi_id,
kodi_type)
self.currentPosition += 1
if seektime:
window('plex_customplaylist.seektime', value=str(seektime))
@ -181,177 +218,145 @@ class PlaybackUtils():
# Ensure that additional parts are played after the main item
self.currentPosition += 1
############### -- CHECK FOR ADDITIONAL PARTS ################
if len(item[0][0]) > 1:
# Only add to the playlist after intros have played
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)
# -- CHECK FOR ADDITIONAL PARTS ################
if len(item[0]) > 1:
self.add_part(item, api, kodi_id, kodi_type)
if dummyPlaylist:
# Added a dummy file to the playlist,
# because the first item is going to fail automatically.
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.
elif propertiesPlayback:
log.debug("Resetting properties playback flag.")
window('plex_playbackProps', clear=True)
#self.pl.verifyPlaylist()
########## SETUP MAIN ITEM ##########
# SETUP MAIN ITEM ##########
# 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):
window('emby_%s.playmethod' % playurl, clear=True)
window('plex_%s.playmethod' % playurl, clear=True)
playurl = tryEncode(playutils.audioSubsPref(
listitem, tryDecode(playurl)))
window('emby_%s.playmethod' % playurl, value="Transcode")
window('plex_%s.playmethod' % playurl, value="Transcode")
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"
and not contextmenu_play):
log.info("Play as a widget item.")
API.CreateListItemFromPlexItem(listitem)
xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem)
log.info("Play as a widget item")
api.CreateListItemFromPlexItem(listitem)
result.listitem = listitem
return result
elif ((introsPlaylist and window('plex_customplaylist') == "true") or
(homeScreen and not sizePlaylist) or
contextmenu_play):
# Playlist was created just now, play it.
# Contextmenu plays always need this
log.info("Play playlist.")
xbmcplugin.endOfDirectory(int(sys.argv[1]), True, False, False)
xbmc.Player().play(kodiPl, startpos=startPos)
log.info("Play playlist from starting position %s" % startPos)
# Need a separate thread because Player won't return in time
thread = Thread(target=Player().play,
args=(playqueue.kodi_pl, None, False, startPos))
thread.setDaemon(True)
thread.start()
# Don't attach listitem
return result
else:
log.info("Play as a regular item.")
xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem)
log.info("Play as a regular item")
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
added
Play all items contained in the xml passed in. Called by Plex Companion
"""
# Failure when downloading trailer playQueue
if xml in (None, 401):
return False
# Failure when getting trailers, e.g. when no plex pass
if xml.attrib.get('size') == '1':
return False
if settings('askCinema') == "true":
resp = xbmcgui.Dialog().yesno(addonName, "Play trailers?")
if not resp:
# User selected to not play trailers
log.info("Skip trailers.")
return False
log.info("Playbackutils play_all called")
window('plex_playbackProps', value="true")
self.currentPosition = 0
for item in self.xml:
api = API(item)
if api.getType() == PLEX_TYPE_CLIP:
self.add_trailer(item)
continue
with Get_Plex_DB() as plex_db:
db_item = plex_db.getItem_byId(api.getRatingKey())
try:
add_item_to_kodi_playlist(self.playqueue,
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!
path = "plugin://plugin.video.plexkodiconnect.movies/"
path = "plugin://plugin.video.plexkodiconnect/movies/"
params = {
'mode': "play",
'dbid': 'plextrailer'
}
for counter, intro in enumerate(xml):
# Don't process the last item - it's the original movie
if counter == len(xml)-1:
break
# The server randomly returns intros, process them.
# introListItem = xbmcgui.ListItem()
# introPlayurl = putils.PlayUtils(intro).getPlayUrl()
introAPI = PlexAPI.API(intro)
params['id'] = introAPI.getRatingKey()
params['filename'] = introAPI.getKey()
introPlayurl = path + '?' + urlencode(params)
log.info("Adding Intro: %s" % introPlayurl)
introAPI = API(item)
listitem = introAPI.CreateListItemFromPlexItem()
params['id'] = introAPI.getRatingKey()
params['filename'] = introAPI.getKey()
introPlayurl = path + '?' + urlencode(params)
introAPI.set_listitem_artwork(listitem)
# Overwrite the Plex url
listitem.setPath(introPlayurl)
log.info("Adding Plex trailer: %s" % introPlayurl)
add_listitem_to_Kodi_playlist(
self.playqueue,
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
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})
api.setPartNumber(0)

View file

@ -11,8 +11,9 @@ from utils import window, settings, language as lang, DateToKodi, \
getUnixTimestamp
import clientinfo
import downloadutils
import embydb_functions as embydb
import plexdb_functions as plexdb
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
window('plex_lastPlayedFiled', value=currentFile)
# 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
while not itemId:
xbmc.sleep(200)
itemId = window("emby_%s.itemid" % currentFile)
itemId = window("plex_%s.itemid" % currentFile)
if count == 5:
log.warn("Could not find itemId, cancelling playback report!")
return
@ -88,16 +89,16 @@ class Player(xbmc.Player):
log.info("ONPLAYBACK_STARTED: %s itemid: %s" % (currentFile, itemId))
embyitem = "emby_%s" % currentFile
runtime = window("%s.runtime" % embyitem)
refresh_id = window("%s.refreshid" % embyitem)
playMethod = window("%s.playmethod" % embyitem)
itemType = window("%s.type" % embyitem)
plexitem = "plex_%s" % currentFile
runtime = window("%s.runtime" % plexitem)
refresh_id = window("%s.refreshid" % plexitem)
playMethod = window("%s.playmethod" % plexitem)
itemType = window("%s.type" % plexitem)
try:
playcount = int(window("%s.playcount" % embyitem))
playcount = int(window("%s.playcount" % plexitem))
except ValueError:
playcount = 0
window('emby_skipWatched%s' % itemId, value="true")
window('plex_skipWatched%s' % itemId, value="true")
log.debug("Playing itemtype is: %s" % itemType)
@ -134,7 +135,7 @@ class Player(xbmc.Player):
volume = result.get('volume')
muted = result.get('muted')
# Postdata structure to send to Emby server
# Postdata structure to send to plex server
url = "{server}/:/timeline?"
postdata = {
@ -154,7 +155,7 @@ class Player(xbmc.Player):
postdata['AudioStreamIndex'] = window("%sAudioStreamIndex" % currentFile)
postdata['SubtitleStreamIndex'] = window("%sSubtitleStreamIndex" % currentFile)
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 = {
"jsonrpc": "2.0",
"id": 1,
@ -190,9 +191,9 @@ class Player(xbmc.Player):
# Postdata for the subtitles
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())
mapping = window("%s.indexMapping" % embyitem)
mapping = window("%s.indexMapping" % plexitem)
if mapping: # Set in playbackutils.py
@ -229,10 +230,10 @@ class Player(xbmc.Player):
log.error('Could not get kodi runtime, setting to zero')
runtime = 0
with embydb.GetEmbyDB() as emby_db:
emby_dbitem = emby_db.getItem_byId(itemId)
with plexdb.Get_Plex_DB() as plex_db:
plex_dbitem = plex_db.getItem_byId(itemId)
try:
fileid = emby_dbitem[1]
fileid = plex_dbitem[1]
except TypeError:
log.info("Could not find fileid in plex db.")
fileid = None
@ -338,7 +339,7 @@ class Player(xbmc.Player):
playMethod = data['playmethod']
# 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:
try:
@ -353,7 +354,7 @@ class Player(xbmc.Player):
if percentComplete >= markPlayed:
# Tell Kodi that we've finished watching (Plex knows)
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:
kodi_db.addPlaystate(
data['fileid'],
@ -391,13 +392,13 @@ class Player(xbmc.Player):
# Clean the WINDOW properties
for filename in self.played_info:
cleanup = (
'emby_%s.itemid' % filename,
'emby_%s.runtime' % filename,
'emby_%s.refreshid' % filename,
'emby_%s.playmethod' % filename,
'emby_%s.type' % filename,
'emby_%s.runtime' % filename,
'emby_%s.playcount' % filename,
'plex_%s.itemid' % filename,
'plex_%s.runtime' % filename,
'plex_%s.refreshid' % filename,
'plex_%s.playmethod' % filename,
'plex_%s.type' % filename,
'plex_%s.runtime' % filename,
'plex_%s.playcount' % filename,
'plex_%s.playlistPosition' % filename
)
for item in cleanup:

View file

@ -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)))

View 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
View 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 ##===----")

View file

@ -45,14 +45,14 @@ class PlayUtils():
log.info("File is direct playing.")
playurl = tryEncode(playurl)
# Set playmethod property
window('emby_%s.playmethod' % playurl, "DirectPlay")
window('plex_%s.playmethod' % playurl, "DirectPlay")
elif self.isDirectStream():
log.info("File is direct streaming.")
playurl = tryEncode(
self.API.getTranscodeVideoPath('DirectStream'))
# Set playmethod property
window('emby_%s.playmethod' % playurl, "DirectStream")
window('plex_%s.playmethod' % playurl, "DirectStream")
else:
log.info("File is transcoding.")
@ -64,7 +64,7 @@ class PlayUtils():
'videoQuality': '100'
}))
# Set playmethod property
window('emby_%s.playmethod' % playurl, value="Transcode")
window('plex_%s.playmethod' % playurl, value="Transcode")
log.info("The playurl is: %s" % playurl)
return playurl

View file

@ -5,7 +5,7 @@ import string
import xbmc
import embydb_functions as embydb
import plexdb_functions as plexdb
###############################################################################
@ -146,11 +146,11 @@ class jsonClass():
def skipTo(self, plexId, typus):
# playlistId = self.getPlaylistId(tryDecode(xbmc_type(typus)))
# playerId = self.
with embydb.GetEmbyDB() as emby_db:
embydb_item = emby_db.getItem_byId(plexId)
with plexdb.Get_Plex_DB() as plex_db:
plexdb_item = plex_db.getItem_byId(plexId)
try:
dbid = embydb_item[0]
mediatype = embydb_item[4]
dbid = plexdb_item[0]
mediatype = plexdb_item[4]
except TypeError:
log.info('Couldnt find item %s in Kodi db' % plexId)
return
@ -163,7 +163,7 @@ class jsonClass():
"Access-Control-Allow-Origin": "*",
"X-Plex-Version": self.settings['version'],
"X-Plex-Client-Identifier": self.settings['uuid'],
"X-Plex-Provides": "player",
"X-Plex-Provides": "client,controller,player",
"X-Plex-Product": "PlexKodiConnect",
"X-Plex-Device-Name": self.settings['client_name'],
"X-Plex-Platform": "Kodi",

View file

@ -72,7 +72,7 @@ class plexgdm:
"Protocol: plex\r\n"
"Protocol-Version: 1\r\n"
"Protocol-Capabilities: timeline,playback,navigation,"
"mirror,playqueues\r\n"
"playqueues\r\n"
"Device-Class: HTPC"
) % (
options['uuid'],

View file

@ -15,7 +15,7 @@ log = logging.getLogger("PLEX."+__name__)
class SubscriptionManager:
def __init__(self, jsonClass, RequestMgr, player, playlist):
def __init__(self, jsonClass, RequestMgr, player, mgr):
self.serverlist = []
self.subscribers = {}
self.info = {}
@ -36,7 +36,7 @@ class SubscriptionManager:
self.playerprops = {}
self.doUtils = downloadutils.DownloadUtils().downloadUrl
self.xbmcplayer = player
self.playlist = playlist
self.playqueue = mgr.playqueue
self.js = jsonClass
self.RequestMgr = RequestMgr
@ -160,8 +160,8 @@ class SubscriptionManager:
with threading.RLock():
for sub in self.subscribers.values():
sub.send_update(msg, len(players) == 0)
self.notifyServer(players)
self.lastplayers = players
self.notifyServer(players)
self.lastplayers = players
return True
def notifyServer(self, players):
@ -231,6 +231,8 @@ class SubscriptionManager:
def getPlayerProperties(self, playerid):
try:
# Get the playqueue
playqueue = self.playqueue.playqueues[playerid]
# get info from the player
props = self.js.jsonrpc(
"Player.GetProperties",
@ -248,18 +250,16 @@ class SubscriptionManager:
'shuffle': ("0", "1")[props.get('shuffled', False)],
'repeat': pf.getPlexRepeat(props.get('repeat')),
}
if self.playlist is not None:
if self.playlist.QueueId() is not None:
info['playQueueID'] = self.playlist.QueueId()
info['playQueueVersion'] = self.playlist.PlayQueueVersion()
info['guid'] = self.playlist.Guid()
# Get the playlist position
pos = self.js.jsonrpc(
"Player.GetProperties",
{"playerid": playerid,
"properties": ["position"]})
info['playQueueItemID'] = \
self.playlist.getQueueIdFromPosition(pos['position'])
if playqueue.ID is not None:
info['playQueueID'] = playqueue.ID
info['playQueueVersion'] = playqueue.version
# Get the playlist position
pos = self.js.jsonrpc(
"Player.GetProperties",
{"playerid": playerid,
"properties": ["position"]})['position']
info['playQueueItemID'] = playqueue.items[pos].ID
info['guid'] = playqueue.items[pos].guid
except:
import traceback
log.error("Traceback:\n%s" % traceback.format_exc())

View 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

View file

@ -101,7 +101,7 @@ class Read_EmbyServer():
viewId = view['Id']
# Compare to view table in emby database
emby = kodiSQL('emby')
emby = kodiSQL('plex')
cursor_emby = emby.cursor()
query = ' '.join((

View file

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

View file

@ -15,7 +15,6 @@ from functools import wraps
from calendar import timegm
import os
import xbmc
import xbmcaddon
import xbmcgui
@ -57,6 +56,24 @@ def window(property, value=None, clear=False, windowid=10000):
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):
"""
Get or add addon setting. Returns unicode
@ -134,21 +151,21 @@ def tryDecode(string, encoding='utf-8'):
def DateToKodi(stamp):
"""
converts a Unix time stamp (seconds passed sinceJanuary 1 1970) to a
propper, human-readable time stamp used by Kodi
"""
converts a Unix time stamp (seconds passed sinceJanuary 1 1970) to a
propper, human-readable time stamp used by Kodi
Output: Y-m-d h:m:s = 2009-04-05 23:16:04
Output: Y-m-d h:m:s = 2009-04-05 23:16:04
None if an error was encountered
"""
try:
stamp = float(stamp) + float(window('kodiplextimeoffset'))
date_time = time.localtime(stamp)
localdate = time.strftime('%Y-%m-%d %H:%M:%S', date_time)
except:
localdate = None
return localdate
None if an error was encountered
"""
try:
stamp = float(stamp) + float(window('kodiplextimeoffset'))
date_time = time.localtime(stamp)
localdate = time.strftime('%Y-%m-%d %H:%M:%S', date_time)
except:
localdate = None
return localdate
def IfExists(path):
@ -200,8 +217,8 @@ def getUnixTimestamp(secondsIntoTheFuture=None):
def kodiSQL(media_type="video"):
if media_type == "emby":
dbPath = tryDecode(xbmc.translatePath("special://database/emby.db"))
if media_type == "plex":
dbPath = tryDecode(xbmc.translatePath("special://database/plex.db"))
elif media_type == "music":
dbPath = getKodiMusicDBPath()
elif media_type == "texture":
@ -346,7 +363,7 @@ def reset():
# Wipe the Plex database
log.info("Resetting the Plex database.")
connection = kodiSQL('emby')
connection = kodiSQL('plex')
cursor = connection.cursor()
cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"')
rows = cursor.fetchall()
@ -939,9 +956,33 @@ def ThreadMethods(cls):
return cls
class Lock_Function:
"""
Decorator for class methods and functions to lock them with lock.
Initialize this class first
lockfunction = Lock_Function(lock), where lock is a threading.Lock() object
To then lock a function or method:
@lockfunction.lockthis
def some_function(args, kwargs)
"""
def __init__(self, lock):
self.lock = lock
def lockthis(self, func):
@wraps(func)
def wrapper(*args, **kwargs):
with self.lock:
result = func(*args, **kwargs)
return result
return wrapper
###############################################################################
# UNUSED METHODS
def changePlayState(itemType, kodiId, playCount, lastplayed):
"""
YET UNUSED
@ -985,3 +1026,27 @@ def changePlayState(itemType, kodiId, playCount, lastplayed):
result = json.loads(result)
result = result.get('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()))

View file

@ -2,13 +2,13 @@
###############################################################################
import logging
import json
import threading
import Queue
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, \
ThreadMethods
@ -22,21 +22,23 @@ log = logging.getLogger("PLEX."+__name__)
@ThreadMethodsAdditionalSuspend('suspend_LibraryThread')
@ThreadMethods
class WebSocket(threading.Thread):
class WebSocket(Thread):
opcode_data = (websocket.ABNF.OPCODE_TEXT, websocket.ABNF.OPCODE_BINARY)
def __init__(self, queue):
def __init__(self, callback=None):
if callback is not None:
self.mgr = callback
self.ws = None
# Communication with librarysync
self.queue = queue
threading.Thread.__init__(self)
self.queue = Queue()
Thread.__init__(self)
def process(self, opcode, message):
if opcode not in self.opcode_data:
return False
try:
message = json.loads(message)
message = loads(message)
except Exception as ex:
log.error('Error decoding message from websocket: %s' % ex)
log.error(message)
@ -57,13 +59,8 @@ class WebSocket(threading.Thread):
return True
# Put PMS message on queue and let libsync take care of it
try:
self.queue.put(message)
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
self.queue.put(message)
return True
def receive(self, ws):
# Not connected yet
@ -97,7 +94,7 @@ class WebSocket(threading.Thread):
uri += '?X-Plex-Token=%s' % token
sslopt = {}
if settings('sslverify') == "false":
sslopt["cert_reqs"] = ssl.CERT_NONE
sslopt["cert_reqs"] = CERT_NONE
log.debug("Uri: %s, sslopt: %s" % (uri, sslopt))
return uri, sslopt
@ -122,7 +119,7 @@ class WebSocket(threading.Thread):
# Abort was requested while waiting. We should exit
log.info("##===---- WebSocketClient Stopped ----===##")
return
xbmc.sleep(1000)
sleep(1000)
try:
self.process(*self.receive(self.ws))
except websocket.WebSocketTimeoutException:
@ -148,11 +145,11 @@ class WebSocket(threading.Thread):
"declaring the connection dead")
window('plex_online', value='false')
counter = 0
xbmc.sleep(1000)
sleep(1000)
except websocket.WebSocketTimeoutException:
log.info("timeout while connecting, trying again")
self.ws = None
xbmc.sleep(1000)
sleep(1000)
except websocket.WebSocketException as e:
log.info('WebSocketException: %s' % e)
if 'Handshake Status 401' in e.args:
@ -162,14 +159,14 @@ class WebSocket(threading.Thread):
'WebSocketClient now')
break
self.ws = None
xbmc.sleep(1000)
sleep(1000)
except Exception as e:
log.error("Unknown exception encountered in connecting: %s"
% e)
import traceback
log.error("Traceback:\n%s" % traceback.format_exc())
self.ws = None
xbmc.sleep(1000)
sleep(1000)
else:
counter = 0
handshake_counter = 0

View file

@ -87,7 +87,7 @@
<category label="30516"><!-- Playback -->
<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="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" />

View file

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