PlexKodiConnect/resources/lib/transfer.py

479 lines
16 KiB
Python
Raw Permalink Normal View History

#!/usr/bin/env python
2017-01-03 00:07:24 +11:00
# -*- coding: utf-8 -*-
"""
Used to shovel data from separate Kodi Python instances to the main thread
and vice versa.
"""
from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
import json
2017-01-03 00:07:24 +11:00
import xbmc
import xbmcgui
LOG = getLogger('PLEX.transfer')
WINDOW = xbmcgui.Window(10000)
WINDOW_UPSTREAM = 'plexkodiconnect.result.upstream'.encode('utf-8')
WINDOW_DOWNSTREAM = 'plexkodiconnect.result.downstream'.encode('utf-8')
WINDOW_COMMAND = 'plexkodiconnect.command'.encode('utf-8')
KODIVERSION = int(xbmc.getInfoLabel("System.BuildVersion")[:2])
def cast(func, value):
"""
2019-06-12 21:17:17 +10:00
Cast the specified value to the specified type (returned by func). Currently
this only support int, float, bool. Should be extended if needed.
Parameters:
func (func): Calback function to used cast to type (int, bool, float).
value (any): value to be cast and returned.
2019-06-12 21:17:17 +10:00
Returns None if something goes wrong
"""
2019-06-12 21:17:17 +10:00
if value is None:
return value
elif func == bool:
return bool(int(value))
elif func == unicode:
if isinstance(value, (int, long, float)):
return unicode(value)
elif isinstance(value, unicode):
return value
else:
return value.decode('utf-8')
elif func == str:
if isinstance(value, (int, long, float)):
return str(value)
elif isinstance(value, str):
return value
else:
return value.encode('utf-8')
elif func == int:
try:
return int(value)
except ValueError:
try:
2019-06-12 21:17:17 +10:00
# Converting e.g. '8.0' fails; need to convert to float first
return int(float(value))
except ValueError:
2019-06-12 21:17:17 +10:00
return
elif func == float:
try:
return float(value)
except ValueError:
return
return func(value)
def kodi_window(property, value=None, clear=False):
"""
Get or set window property - thread safe! value must be string
"""
if clear:
WINDOW.clearProperty(property)
elif value is not None:
WINDOW.setProperty(property, value)
else:
return WINDOW.getProperty(property)
def plex_command(value):
"""
Used to funnel states between different Python instances. NOT really thread
safe - let's hope the Kodi user can't click fast enough
"""
while kodi_window(WINDOW_COMMAND):
xbmc.sleep(50)
kodi_window(WINDOW_COMMAND, value=value)
def serialize(obj):
if isinstance(obj, PKCListItem):
return {'type': 'PKCListItem', 'data': obj.data}
else:
return {'type': 'other', 'data': obj}
return
def de_serialize(answ):
if answ['type'] == 'PKCListItem':
result = PKCListItem()
result.data = answ['data']
return convert_pkc_to_listitem(result)
elif answ['type'] == 'other':
return answ['data']
else:
raise NotImplementedError('Not implemented: %s' % answ)
def send(pkc_listitem, target='default'):
"""
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
Set target='default' if you send data TO another Python default.py
instance, 'main' if your default.py needs to send to the main thread
"""
window = WINDOW_DOWNSTREAM if target == 'default' else WINDOW_UPSTREAM
LOG.debug('Sending: %s', pkc_listitem)
kodi_window(window,
value=json.dumps(serialize(pkc_listitem)))
def wait_for_transfer(source='main'):
"""
Set source='default' if you wait for data FROM another Python default.py
instance, 'main' if your default.py needs to wait for the main thread
"""
LOG.debug('Waiting for transfer from %s', source)
window = WINDOW_DOWNSTREAM if source == 'main' else WINDOW_UPSTREAM
result = ''
while not result:
result = kodi_window(window)
if result:
kodi_window(window, clear=True)
LOG.debug('Received')
result = json.loads(result)
return de_serialize(result)
xbmc.sleep(50)
2017-01-03 00:07:24 +11:00
2018-06-22 03:24:37 +10:00
def convert_pkc_to_listitem(pkc_listitem):
2017-01-03 00:07:24 +11:00
"""
2018-06-22 03:24:37 +10:00
Insert a PKCListItem() and you will receive a valid XBMC listitem
2017-01-03 00:07:24 +11:00
"""
2018-06-22 03:24:37 +10:00
data = pkc_listitem.data
if KODIVERSION >= 18:
listitem = xbmcgui.ListItem(label=data.get('label'),
label2=data.get('label2'),
path=data.get('path'),
offscreen=True)
else:
listitem = xbmcgui.ListItem(label=data.get('label'),
label2=data.get('label2'),
path=data.get('path'))
2017-03-19 22:14:16 +11:00
if data['info']:
listitem.setInfo(**data['info'])
for stream in data['stream_info']:
# Kodi documentation up to date? CAREFUL as type= seems to be cType=
# and values= seems to be dictionary=
2017-03-19 22:14:16 +11:00
listitem.addStreamInfo(**stream)
if data['art']:
listitem.setArt(data['art'])
for key, value in data['property'].iteritems():
listitem.setProperty(key, cast(str, value))
2017-03-19 22:14:16 +11:00
if data['subtitles']:
listitem.setSubtitles(data['subtitles'])
2019-06-11 05:29:42 +10:00
if data['contextmenu']:
listitem.addContextMenuItems(data['contextmenu'])
2017-01-03 00:07:24 +11:00
return listitem
2018-06-22 03:24:37 +10:00
class PKCListItem(object):
2017-01-03 00:07:24 +11:00
"""
Imitates xbmcgui.ListItem and its functions. Pass along PKC_Listitem().data
when pickling!
WARNING: set/get path only via setPath and getPath! (not getProperty)
"""
2019-06-11 05:29:42 +10:00
def __init__(self, label=None, label2=None, path=None, offscreen=True):
2017-01-03 00:07:24 +11:00
self.data = {
2017-03-19 22:14:16 +11:00
'stream_info': [], # (type, values: dict { label: value })
'art': {}, # dict
'info': {}, # type: infoLabel (dict { label: value })
'label': label, # string
'label2': label2, # string
'path': path, # string
'property': {}, # (key, value)
'subtitles': [], # strings
2019-06-11 05:29:42 +10:00
'contextmenu': None
2017-01-03 00:07:24 +11:00
}
2019-06-11 05:29:42 +10:00
def addContextMenuItems(self, items):
2017-01-03 00:07:24 +11:00
"""
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.
"""
2019-06-11 05:29:42 +10:00
self.data['contextmenu'] = items
2017-01-03 00:07:24 +11:00
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['stream_info'].append({'cType': type, 'dictionary': values})
2017-01-03 00:07:24 +11:00
def getLabel(self):
"""
Returns the listitem label
"""
2017-03-19 22:14:16 +11:00
return self.data.get('label')
2017-01-03 00:07:24 +11:00
def getLabel2(self):
"""
Returns the listitem label.
"""
2017-03-19 22:14:16 +11:00
return self.data.get('label2')
2017-01-03 00:07:24 +11:00
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.
"""
2017-03-19 22:14:16 +11:00
return self.data['property'].get(key)
2017-01-03 00:07:24 +11:00
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
"""
2017-03-19 22:14:16 +11:00
self.data['art'].update(values)
2017-01-03 00:07:24 +11:00
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)
"""
2017-03-19 22:14:16 +11:00
self.data['info'] = {'type': type, 'infoLabels': infoLabels}
2017-01-03 00:07:24 +11:00
def setLabel(self, label):
"""
Sets the listitem's label.
label : string or unicode - text string.
"""
2017-03-19 22:14:16 +11:00
self.data['label'] = label
2017-01-03 00:07:24 +11:00
def setLabel2(self, label):
"""
Sets the listitem's label2.
label : string or unicode - text string.
"""
2017-03-19 22:14:16 +11:00
self.data['label2'] = label
2017-01-03 00:07:24 +11:00
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.
"""
2017-03-19 22:14:16 +11:00
self.data['path'] = path
2017-01-03 00:07:24 +11:00
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
"""
2017-03-19 22:14:16 +11:00
self.data['property'][key] = value
2017-01-03 00:07:24 +11:00
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' ])
"""
2017-03-19 22:14:16 +11:00
self.data['subtitles'].extend(subtitles)