commit
6376fecbf7
21 changed files with 236 additions and 280 deletions
|
@ -1,5 +1,5 @@
|
||||||
[![stable version](https://img.shields.io/badge/stable_version-2.9.11-blue.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/stable/repository.plexkodiconnect/repository.plexkodiconnect-1.0.2.zip)
|
[![stable version](https://img.shields.io/badge/stable_version-2.10.0-blue.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/stable/repository.plexkodiconnect/repository.plexkodiconnect-1.0.2.zip)
|
||||||
[![beta version](https://img.shields.io/badge/beta_version-2.9.11-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip)
|
[![beta version](https://img.shields.io/badge/beta_version-2.10.0-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip)
|
||||||
|
|
||||||
[![Installation](https://img.shields.io/badge/wiki-installation-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/Installation)
|
[![Installation](https://img.shields.io/badge/wiki-installation-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/Installation)
|
||||||
[![FAQ](https://img.shields.io/badge/wiki-FAQ-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/faq)
|
[![FAQ](https://img.shields.io/badge/wiki-FAQ-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/faq)
|
||||||
|
@ -50,8 +50,8 @@ Some people argue that PKC is 'hacky' because of the way it directly accesses th
|
||||||
|
|
||||||
### PKC Features
|
### PKC Features
|
||||||
|
|
||||||
|
- Kodi 19 Matrix is not yet supported (PKC is written in Python 2)
|
||||||
- Support for Kodi 18 Leia
|
- Support for Kodi 18 Leia
|
||||||
- Support for Kodi 17 Krypton
|
|
||||||
- [Amazon Alexa voice recognition](https://www.plex.tv/apps/streaming-devices/amazon-alexa)
|
- [Amazon Alexa voice recognition](https://www.plex.tv/apps/streaming-devices/amazon-alexa)
|
||||||
- [Cinema Trailers & Extras](https://support.plex.tv/articles/202934883-cinema-trailers-extras/)
|
- [Cinema Trailers & Extras](https://support.plex.tv/articles/202934883-cinema-trailers-extras/)
|
||||||
- [Plex Watch Later / Plex It!](https://support.plex.tv/hc/en-us/sections/200211783-Plex-It-)
|
- [Plex Watch Later / Plex It!](https://support.plex.tv/hc/en-us/sections/200211783-Plex-It-)
|
||||||
|
|
28
addon.xml
28
addon.xml
|
@ -1,11 +1,11 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
<addon id="plugin.video.plexkodiconnect" name="PlexKodiConnect" version="2.9.11" provider-name="croneter">
|
<addon id="plugin.video.plexkodiconnect" name="PlexKodiConnect" version="2.10.0" provider-name="croneter">
|
||||||
<requires>
|
<requires>
|
||||||
<import addon="xbmc.python" version="2.1.0"/>
|
<import addon="xbmc.python" version="2.1.0"/>
|
||||||
<import addon="script.module.requests" version="2.9.1" />
|
<import addon="script.module.requests" version="2.9.1" />
|
||||||
<import addon="script.module.defusedxml" version="0.5.0"/>
|
<import addon="script.module.defusedxml" version="0.5.0"/>
|
||||||
<import addon="plugin.video.plexkodiconnect.movies" version="2.1.1" />
|
<import addon="plugin.video.plexkodiconnect.movies" version="2.1.2" />
|
||||||
<import addon="plugin.video.plexkodiconnect.tvshows" version="2.1.1" />
|
<import addon="plugin.video.plexkodiconnect.tvshows" version="2.1.2" />
|
||||||
</requires>
|
</requires>
|
||||||
<extension point="xbmc.python.pluginsource" library="default.py">
|
<extension point="xbmc.python.pluginsource" library="default.py">
|
||||||
<provides>video audio image</provides>
|
<provides>video audio image</provides>
|
||||||
|
@ -83,7 +83,27 @@
|
||||||
<summary lang="lt_LT">Natūralioji „Plex“ integracija į „Kodi“</summary>
|
<summary lang="lt_LT">Natūralioji „Plex“ integracija į „Kodi“</summary>
|
||||||
<description lang="lt_LT">Prijunkite „Kodi“ prie „Plex Medija Serverio“. Šiame papildinyje daroma prielaida, kad valdote visus savo vaizdo įrašus naudodami „Plex“ (ir nė vieno su „Kodi“). Galite prarasti jau saugomus „Kodi“ vaizdo įrašų ir muzikos duomenų bazių duomenis (kadangi šis papildinys juos tiesiogiai pakeičia). Naudokite savo pačių rizika!</description>
|
<description lang="lt_LT">Prijunkite „Kodi“ prie „Plex Medija Serverio“. Šiame papildinyje daroma prielaida, kad valdote visus savo vaizdo įrašus naudodami „Plex“ (ir nė vieno su „Kodi“). Galite prarasti jau saugomus „Kodi“ vaizdo įrašų ir muzikos duomenų bazių duomenis (kadangi šis papildinys juos tiesiogiai pakeičia). Naudokite savo pačių rizika!</description>
|
||||||
<disclaimer lang="lt_LT">Naudokite savo pačių rizika</disclaimer>
|
<disclaimer lang="lt_LT">Naudokite savo pačių rizika</disclaimer>
|
||||||
<news>version 2.9.11:
|
<news>version 2.10.0:
|
||||||
|
- version 2.9.12 - 2.9.14 for everyone
|
||||||
|
- Get rid of some obsolete code for the ContextMonitor we dropped
|
||||||
|
|
||||||
|
version 2.9.14 (beta only):
|
||||||
|
- Fix resume when starting playback via PMS or when force transcoding
|
||||||
|
- Get rid of ContextMonitor and the dedicated Python thread - with new resume mechanics, this is not needed anymore
|
||||||
|
- Optimize clean-up of file table in the Kodi video database after stopping playback
|
||||||
|
- Get rid of some obsolete imports
|
||||||
|
|
||||||
|
version 2.9.13 (beta only):
|
||||||
|
- Fix PKC resuming instead of playing from the beginning
|
||||||
|
|
||||||
|
version 2.9.12 (beta only):
|
||||||
|
- Fix resume not working in some cases
|
||||||
|
- Support Plex search across all media and Plex Media Servers: Navigate to the PlexKodiConnect Add-on, then "Search"
|
||||||
|
- Always use the current Kodi language when communicating with the PMS (restart Kodi when changing the language!)
|
||||||
|
- Fix Kodi crashing when casting from e.g. Plex Web or Plex for Windows
|
||||||
|
- Fix PKC throwing error if m3u playlist contains resume information
|
||||||
|
|
||||||
|
version 2.9.11:
|
||||||
- version 2.9.10 for everyone
|
- version 2.9.10 for everyone
|
||||||
|
|
||||||
version 2.9.10 (beta only):
|
version 2.9.10 (beta only):
|
||||||
|
|
|
@ -1,3 +1,23 @@
|
||||||
|
version 2.10.0:
|
||||||
|
- version 2.9.12 - 2.9.14 for everyone
|
||||||
|
- Get rid of some obsolete code for the ContextMonitor we dropped
|
||||||
|
|
||||||
|
version 2.9.14 (beta only):
|
||||||
|
- Fix resume when starting playback via PMS or when force transcoding
|
||||||
|
- Get rid of ContextMonitor and the dedicated Python thread - with new resume mechanics, this is not needed anymore
|
||||||
|
- Optimize clean-up of file table in the Kodi video database after stopping playback
|
||||||
|
- Get rid of some obsolete imports
|
||||||
|
|
||||||
|
version 2.9.13 (beta only):
|
||||||
|
- Fix PKC resuming instead of playing from the beginning
|
||||||
|
|
||||||
|
version 2.9.12 (beta only):
|
||||||
|
- Fix resume not working in some cases
|
||||||
|
- Support Plex search across all media and Plex Media Servers: Navigate to the PlexKodiConnect Add-on, then "Search"
|
||||||
|
- Always use the current Kodi language when communicating with the PMS (restart Kodi when changing the language!)
|
||||||
|
- Fix Kodi crashing when casting from e.g. Plex Web or Plex for Windows
|
||||||
|
- Fix PKC throwing error if m3u playlist contains resume information
|
||||||
|
|
||||||
version 2.9.11:
|
version 2.9.11:
|
||||||
- version 2.9.10 for everyone
|
- version 2.9.10 for everyone
|
||||||
|
|
||||||
|
|
|
@ -61,6 +61,13 @@ class Main():
|
||||||
elif mode == 'channels':
|
elif mode == 'channels':
|
||||||
entrypoint.browse_plex(key='/channels/all')
|
entrypoint.browse_plex(key='/channels/all')
|
||||||
|
|
||||||
|
elif mode == 'search':
|
||||||
|
# "Search"
|
||||||
|
entrypoint.browse_plex(key='/hubs/search',
|
||||||
|
args={'includeCollections': 1,
|
||||||
|
'includeExternalMedia': 1},
|
||||||
|
prompt=utils.lang(137))
|
||||||
|
|
||||||
elif mode == 'route_to_extras':
|
elif mode == 'route_to_extras':
|
||||||
# Hack so we can store this path in the Kodi DB
|
# Hack so we can store this path in the Kodi DB
|
||||||
handle = ('plugin://%s?mode=extras&plex_id=%s'
|
handle = ('plugin://%s?mode=extras&plex_id=%s'
|
||||||
|
|
|
@ -56,12 +56,6 @@ class PlayState(object):
|
||||||
# Currently playing PKC item, a PlaylistItem()
|
# Currently playing PKC item, a PlaylistItem()
|
||||||
self.item = None
|
self.item = None
|
||||||
|
|
||||||
# Set by SpecialMonitor - did user choose to resume playback or start from the
|
|
||||||
# beginning?
|
|
||||||
# Set to None if resume dialog has not been shown
|
|
||||||
# True if dialog has been shown and user selected to resume
|
|
||||||
# False if dialog has been shown and user chose to start from beginning
|
|
||||||
self.resume_playback = None
|
|
||||||
# Was the playback initiated by the user using the Kodi context menu?
|
# Was the playback initiated by the user using the Kodi context menu?
|
||||||
self.context_menu_play = False
|
self.context_menu_play = False
|
||||||
# Set by context menu - shall we force-transcode the next playing item?
|
# Set by context menu - shall we force-transcode the next playing item?
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
from __future__ import absolute_import, division, unicode_literals
|
from __future__ import absolute_import, division, unicode_literals
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
|
|
||||||
|
import xbmc
|
||||||
|
|
||||||
from . import utils
|
from . import utils
|
||||||
from . import variables as v
|
from . import variables as v
|
||||||
|
|
||||||
|
@ -31,7 +33,7 @@ def getXArgsDeviceInfo(options=None, include_token=True):
|
||||||
'Connection': 'keep-alive',
|
'Connection': 'keep-alive',
|
||||||
"Content-Type": "application/x-www-form-urlencoded",
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
# "Access-Control-Allow-Origin": "*",
|
# "Access-Control-Allow-Origin": "*",
|
||||||
# 'X-Plex-Language': 'en',
|
'Accept-Language': xbmc.getLanguage(xbmc.ISO_639_1),
|
||||||
'X-Plex-Device': v.DEVICE,
|
'X-Plex-Device': v.DEVICE,
|
||||||
'X-Plex-Model': v.MODEL,
|
'X-Plex-Model': v.MODEL,
|
||||||
'X-Plex-Device-Name': v.DEVICENAME,
|
'X-Plex-Device-Name': v.DEVICENAME,
|
||||||
|
|
|
@ -146,6 +146,8 @@ def show_main_menu(content_type=None):
|
||||||
if content_type:
|
if content_type:
|
||||||
path += '&content_type=%s' % content_type
|
path += '&content_type=%s' % content_type
|
||||||
directory_item('Plex Hub', path)
|
directory_item('Plex Hub', path)
|
||||||
|
# Plex Search "Search"
|
||||||
|
directory_item(utils.lang(137), "plugin://%s?mode=search" % v.ADDON_ID)
|
||||||
# Plex Watch later
|
# Plex Watch later
|
||||||
if content_type not in ('image', 'audio'):
|
if content_type not in ('image', 'audio'):
|
||||||
directory_item(utils.lang(39211),
|
directory_item(utils.lang(39211),
|
||||||
|
@ -466,7 +468,7 @@ def watchlater():
|
||||||
|
|
||||||
|
|
||||||
def browse_plex(key=None, plex_type=None, section_id=None, synched=True,
|
def browse_plex(key=None, plex_type=None, section_id=None, synched=True,
|
||||||
prompt=None):
|
args=None, prompt=None):
|
||||||
"""
|
"""
|
||||||
Lists the content of a Plex folder, e.g. channels. Either pass in key (to
|
Lists the content of a Plex folder, e.g. channels. Either pass in key (to
|
||||||
be used directly for PMS url {server}<key>) or the section_id
|
be used directly for PMS url {server}<key>) or the section_id
|
||||||
|
@ -474,28 +476,43 @@ def browse_plex(key=None, plex_type=None, section_id=None, synched=True,
|
||||||
Pass synched=False if the items have NOT been synched to the Kodi DB
|
Pass synched=False if the items have NOT been synched to the Kodi DB
|
||||||
"""
|
"""
|
||||||
LOG.debug('Browsing to key %s, section %s, plex_type: %s, synched: %s, '
|
LOG.debug('Browsing to key %s, section %s, plex_type: %s, synched: %s, '
|
||||||
'prompt "%s"', key, section_id, plex_type, synched, prompt)
|
'prompt "%s", args %s', key, section_id, plex_type, synched,
|
||||||
|
prompt, args)
|
||||||
if not _wait_for_auth():
|
if not _wait_for_auth():
|
||||||
xbmcplugin.endOfDirectory(int(sys.argv[1]), False)
|
xbmcplugin.endOfDirectory(int(sys.argv[1]), False)
|
||||||
return
|
return
|
||||||
app.init(entrypoint=True)
|
app.init(entrypoint=True)
|
||||||
|
args = args or {}
|
||||||
if prompt:
|
if prompt:
|
||||||
prompt = utils.dialog('input', prompt)
|
prompt = utils.dialog('input', prompt)
|
||||||
if prompt is None:
|
if prompt is None:
|
||||||
# User cancelled
|
# User cancelled
|
||||||
return
|
return
|
||||||
prompt = prompt.strip().decode('utf-8')
|
prompt = prompt.strip().decode('utf-8')
|
||||||
if '?' not in key:
|
args['query'] = prompt
|
||||||
key = '%s?query=%s' % (key, prompt)
|
xml = DU().downloadUrl(utils.extend_url('{server}%s' % key, args))
|
||||||
else:
|
|
||||||
key = '%s&query=%s' % (key, prompt)
|
|
||||||
xml = DU().downloadUrl('{server}%s' % key)
|
|
||||||
try:
|
try:
|
||||||
xml.attrib
|
xml[0].attrib
|
||||||
except AttributeError:
|
except (TypeError, IndexError, AttributeError):
|
||||||
LOG.error('Could not browse to key %s, section %s',
|
LOG.error('Could not browse to key %s, section %s',
|
||||||
key, section_id)
|
key, section_id)
|
||||||
return
|
return
|
||||||
|
if xml[0].tag == 'Hub':
|
||||||
|
# E.g. when hitting the endpoint '/hubs/search'
|
||||||
|
answ = utils.etree.Element(xml.tag, attrib=xml.attrib)
|
||||||
|
for hub in xml:
|
||||||
|
if not utils.cast(int, hub.get('size')):
|
||||||
|
# Empty category
|
||||||
|
continue
|
||||||
|
for entry in hub:
|
||||||
|
api = API(entry)
|
||||||
|
if api.plex_type == v.PLEX_TYPE_TAG:
|
||||||
|
# Append the type before the actual element for all "tags"
|
||||||
|
# like genres, actors, etc.
|
||||||
|
entry.attrib['tag'] = '%s: %s' % (hub.get('title'),
|
||||||
|
api.tag_label())
|
||||||
|
answ.append(entry)
|
||||||
|
xml = answ
|
||||||
show_listing(xml, plex_type, section_id, synched, key)
|
show_listing(xml, plex_type, section_id, synched, key)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ from logging import getLogger
|
||||||
from sqlite3 import IntegrityError
|
from sqlite3 import IntegrityError
|
||||||
|
|
||||||
from . import common
|
from . import common
|
||||||
from .. import path_ops, timing, variables as v, app
|
from .. import path_ops, timing, variables as v
|
||||||
|
|
||||||
LOG = getLogger('PLEX.kodi_db.video')
|
LOG = getLogger('PLEX.kodi_db.video')
|
||||||
|
|
||||||
|
|
|
@ -11,21 +11,17 @@ import json
|
||||||
import binascii
|
import binascii
|
||||||
|
|
||||||
import xbmc
|
import xbmc
|
||||||
import xbmcgui
|
|
||||||
|
|
||||||
from .plex_api import API
|
from .plex_api import API
|
||||||
from .plex_db import PlexDB
|
from .plex_db import PlexDB
|
||||||
from . import kodi_db
|
from . import kodi_db
|
||||||
from .downloadutils import DownloadUtils as DU
|
from .downloadutils import DownloadUtils as DU
|
||||||
from . import utils, timing, plex_functions as PF, playback
|
from . import utils, timing, plex_functions as PF
|
||||||
from . import json_rpc as js, playqueue as PQ, playlist_func as PL
|
from . import json_rpc as js, playqueue as PQ, playlist_func as PL
|
||||||
from . import backgroundthread, app, variables as v
|
from . import backgroundthread, app, variables as v
|
||||||
|
|
||||||
LOG = getLogger('PLEX.kodimonitor')
|
LOG = getLogger('PLEX.kodimonitor')
|
||||||
|
|
||||||
# "Start from beginning", "Play from beginning"
|
|
||||||
STRINGS = (utils.lang(12021).encode('utf-8'), utils.lang(12023).encode('utf-8'))
|
|
||||||
|
|
||||||
|
|
||||||
class KodiMonitor(xbmc.Monitor):
|
class KodiMonitor(xbmc.Monitor):
|
||||||
"""
|
"""
|
||||||
|
@ -33,7 +29,6 @@ class KodiMonitor(xbmc.Monitor):
|
||||||
"""
|
"""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._already_slept = False
|
self._already_slept = False
|
||||||
self.hack_replay = None
|
|
||||||
xbmc.Monitor.__init__(self)
|
xbmc.Monitor.__init__(self)
|
||||||
for playerid in app.PLAYSTATE.player_states:
|
for playerid in app.PLAYSTATE.player_states:
|
||||||
app.PLAYSTATE.player_states[playerid] = copy.deepcopy(app.PLAYSTATE.template)
|
app.PLAYSTATE.player_states[playerid] = copy.deepcopy(app.PLAYSTATE.template)
|
||||||
|
@ -57,9 +52,6 @@ class KodiMonitor(xbmc.Monitor):
|
||||||
Monitor the PKC settings for changes made by the user
|
Monitor the PKC settings for changes made by the user
|
||||||
"""
|
"""
|
||||||
LOG.debug('PKC settings change detected')
|
LOG.debug('PKC settings change detected')
|
||||||
# Assume that the user changed something so we can try to reconnect
|
|
||||||
# app.APP.suspend = False
|
|
||||||
# app.APP.resume_threads(block=False)
|
|
||||||
|
|
||||||
def onNotification(self, sender, method, data):
|
def onNotification(self, sender, method, data):
|
||||||
"""
|
"""
|
||||||
|
@ -69,28 +61,12 @@ class KodiMonitor(xbmc.Monitor):
|
||||||
data = loads(data, 'utf-8')
|
data = loads(data, 'utf-8')
|
||||||
LOG.debug("Method: %s Data: %s", method, data)
|
LOG.debug("Method: %s Data: %s", method, data)
|
||||||
|
|
||||||
# Hack
|
|
||||||
if not method == 'Player.OnStop':
|
|
||||||
self.hack_replay = None
|
|
||||||
|
|
||||||
if method == "Player.OnPlay":
|
if method == "Player.OnPlay":
|
||||||
with app.APP.lock_playqueues:
|
with app.APP.lock_playqueues:
|
||||||
self.PlayBackStart(data)
|
self.PlayBackStart(data)
|
||||||
elif method == "Player.OnStop":
|
elif method == "Player.OnStop":
|
||||||
# Should refresh our video nodes, e.g. on deck
|
with app.APP.lock_playqueues:
|
||||||
# xbmc.executebuiltin('ReloadSkin()')
|
_playback_cleanup(ended=data.get('end'))
|
||||||
if (self.hack_replay and not data.get('end') and
|
|
||||||
self.hack_replay == data['item']):
|
|
||||||
# Hack for add-on paths
|
|
||||||
self.hack_replay = None
|
|
||||||
with app.APP.lock_playqueues:
|
|
||||||
self._hack_addon_paths_replay_video()
|
|
||||||
elif data.get('end'):
|
|
||||||
with app.APP.lock_playqueues:
|
|
||||||
_playback_cleanup(ended=True)
|
|
||||||
else:
|
|
||||||
with app.APP.lock_playqueues:
|
|
||||||
_playback_cleanup()
|
|
||||||
elif method == 'Playlist.OnAdd':
|
elif method == 'Playlist.OnAdd':
|
||||||
if 'item' in data and data['item'].get('type') == v.KODI_TYPE_SHOW:
|
if 'item' in data and data['item'].get('type') == v.KODI_TYPE_SHOW:
|
||||||
# Hitting the "browse" button on tv show info dialog
|
# Hitting the "browse" button on tv show info dialog
|
||||||
|
@ -126,39 +102,6 @@ class KodiMonitor(xbmc.Monitor):
|
||||||
elif method == 'Other.plugin.video.plexkodiconnect_play_action':
|
elif method == 'Other.plugin.video.plexkodiconnect_play_action':
|
||||||
self._start_next_episode(data)
|
self._start_next_episode(data)
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _hack_addon_paths_replay_video():
|
|
||||||
"""
|
|
||||||
Hack we need for RESUMABLE items because Kodi lost the path of the
|
|
||||||
last played item that is now being replayed (see playback.py's
|
|
||||||
Player().play()) Also see playqueue.py _compare_playqueues()
|
|
||||||
|
|
||||||
Needed if user re-starts the same video from the library using addon
|
|
||||||
paths. (Video is only added to playqueue, then immediately stoppen.
|
|
||||||
There is no playback initialized by Kodi.) Log excerpts:
|
|
||||||
Method: Playlist.OnAdd Data:
|
|
||||||
{u'item': {u'type': u'movie', u'id': 4},
|
|
||||||
u'playlistid': 1,
|
|
||||||
u'position': 0}
|
|
||||||
Now we would hack!
|
|
||||||
Method: Player.OnStop Data:
|
|
||||||
{u'item': {u'type': u'movie', u'id': 4},
|
|
||||||
u'end': False}
|
|
||||||
(within the same micro-second!)
|
|
||||||
"""
|
|
||||||
LOG.info('Detected re-start of playback of last item')
|
|
||||||
old = app.PLAYSTATE.old_player_states[1]
|
|
||||||
kwargs = {
|
|
||||||
'plex_id': old['plex_id'],
|
|
||||||
'plex_type': old['plex_type'],
|
|
||||||
'path': old['file'],
|
|
||||||
'resolve': False
|
|
||||||
}
|
|
||||||
task = backgroundthread.FunctionAsTask(playback.playback_triage,
|
|
||||||
None,
|
|
||||||
**kwargs)
|
|
||||||
backgroundthread.BGThreader.addTasksToFront([task])
|
|
||||||
|
|
||||||
def _playlist_onadd(self, data):
|
def _playlist_onadd(self, data):
|
||||||
"""
|
"""
|
||||||
Called if an item is added to a Kodi playlist. Example data dict:
|
Called if an item is added to a Kodi playlist. Example data dict:
|
||||||
|
@ -171,15 +114,7 @@ class KodiMonitor(xbmc.Monitor):
|
||||||
}
|
}
|
||||||
Will NOT be called if playback initiated by Kodi widgets
|
Will NOT be called if playback initiated by Kodi widgets
|
||||||
"""
|
"""
|
||||||
if 'id' not in data['item']:
|
pass
|
||||||
return
|
|
||||||
old = app.PLAYSTATE.old_player_states[data['playlistid']]
|
|
||||||
if (not app.SYNC.direct_paths and
|
|
||||||
data['position'] == 0 and data['playlistid'] == 1 and
|
|
||||||
not PQ.PLAYQUEUES[data['playlistid']].items and
|
|
||||||
data['item']['type'] == old['kodi_type'] and
|
|
||||||
data['item']['id'] == old['kodi_id']):
|
|
||||||
self.hack_replay = data['item']
|
|
||||||
|
|
||||||
def _playlist_onremove(self, data):
|
def _playlist_onremove(self, data):
|
||||||
"""
|
"""
|
||||||
|
@ -451,7 +386,7 @@ def _playback_cleanup(ended=False):
|
||||||
app.PLAYSTATE.active_players = set()
|
app.PLAYSTATE.active_players = set()
|
||||||
app.PLAYSTATE.item = None
|
app.PLAYSTATE.item = None
|
||||||
utils.delete_temporary_subtitles()
|
utils.delete_temporary_subtitles()
|
||||||
LOG.info('Finished PKC playback cleanup')
|
LOG.debug('Finished PKC playback cleanup')
|
||||||
|
|
||||||
|
|
||||||
def _record_playstate(status, ended):
|
def _record_playstate(status, ended):
|
||||||
|
@ -528,13 +463,15 @@ def _clean_file_table():
|
||||||
This function tries for at most 5 seconds to clean the file table.
|
This function tries for at most 5 seconds to clean the file table.
|
||||||
"""
|
"""
|
||||||
LOG.debug('Start cleaning Kodi files table')
|
LOG.debug('Start cleaning Kodi files table')
|
||||||
app.APP.monitor.waitForAbort(2)
|
if app.APP.monitor.waitForAbort(2):
|
||||||
|
# PKC should exit
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
with kodi_db.KodiVideoDB() as kodidb_1:
|
with kodi_db.KodiVideoDB() as kodidb:
|
||||||
with kodi_db.KodiVideoDB(lock=False) as kodidb_2:
|
obsolete_file_ids = list(kodidb.obsolete_file_ids())
|
||||||
for file_id in kodidb_1.obsolete_file_ids():
|
for file_id in obsolete_file_ids:
|
||||||
LOG.debug('Removing obsolete Kodi file_id %s', file_id)
|
LOG.debug('Removing obsolete Kodi file_id %s', file_id)
|
||||||
kodidb_2.remove_file(file_id, remove_orphans=False)
|
kodidb.remove_file(file_id, remove_orphans=False)
|
||||||
except utils.OperationalError:
|
except utils.OperationalError:
|
||||||
LOG.debug('Database was locked, unable to clean file table')
|
LOG.debug('Database was locked, unable to clean file table')
|
||||||
else:
|
else:
|
||||||
|
@ -653,36 +590,3 @@ def _videolibrary_onupdate(data):
|
||||||
PF.scrobble(db_item['plex_id'], 'watched')
|
PF.scrobble(db_item['plex_id'], 'watched')
|
||||||
else:
|
else:
|
||||||
PF.scrobble(db_item['plex_id'], 'unwatched')
|
PF.scrobble(db_item['plex_id'], 'unwatched')
|
||||||
|
|
||||||
|
|
||||||
class ContextMonitor(backgroundthread.KillableThread):
|
|
||||||
"""
|
|
||||||
Detect the resume dialog for widgets. Could also be used to detect
|
|
||||||
external players (see Emby implementation)
|
|
||||||
|
|
||||||
Let's not register this thread because it won't quit due to
|
|
||||||
xbmc.getCondVisibility
|
|
||||||
It should still exit at some point due to xbmc.abortRequested
|
|
||||||
"""
|
|
||||||
def run(self):
|
|
||||||
LOG.info("----===## Starting ContextMonitor ##===----")
|
|
||||||
# app.APP.register_thread(self)
|
|
||||||
try:
|
|
||||||
self._run()
|
|
||||||
finally:
|
|
||||||
# app.APP.deregister_thread(self)
|
|
||||||
LOG.info("##===---- ContextMonitor Stopped ----===##")
|
|
||||||
|
|
||||||
def _run(self):
|
|
||||||
while not self.isCanceled():
|
|
||||||
# The following function will block if called while PKC should
|
|
||||||
# exit!
|
|
||||||
if xbmc.getCondVisibility('Window.IsVisible(DialogContextMenu.xml)'):
|
|
||||||
if xbmc.getInfoLabel('Control.GetLabel(1002)') in STRINGS:
|
|
||||||
# Remember that the item IS indeed resumable
|
|
||||||
control = int(xbmcgui.Window(10106).getFocusId())
|
|
||||||
app.PLAYSTATE.resume_playback = True if control == 1001 else False
|
|
||||||
else:
|
|
||||||
# Different context menu is displayed
|
|
||||||
app.PLAYSTATE.resume_playback = False
|
|
||||||
xbmc.sleep(100)
|
|
||||||
|
|
|
@ -30,7 +30,8 @@ RESOLVE = True
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
|
||||||
def playback_triage(plex_id=None, plex_type=None, path=None, resolve=True):
|
def playback_triage(plex_id=None, plex_type=None, path=None, resolve=True,
|
||||||
|
resume=False):
|
||||||
"""
|
"""
|
||||||
Hit this function for addon path playback, Plex trailers, etc.
|
Hit this function for addon path playback, Plex trailers, etc.
|
||||||
Will setup playback first, then on second call complete playback.
|
Will setup playback first, then on second call complete playback.
|
||||||
|
@ -47,19 +48,18 @@ def playback_triage(plex_id=None, plex_type=None, path=None, resolve=True):
|
||||||
service.py Python instance
|
service.py Python instance
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
_playback_triage(plex_id, plex_type, path, resolve)
|
_playback_triage(plex_id, plex_type, path, resolve, resume)
|
||||||
finally:
|
finally:
|
||||||
# Reset some playback variables the user potentially set to init
|
# Reset some playback variables the user potentially set to init
|
||||||
# playback
|
# playback
|
||||||
app.PLAYSTATE.context_menu_play = False
|
app.PLAYSTATE.context_menu_play = False
|
||||||
app.PLAYSTATE.force_transcode = False
|
app.PLAYSTATE.force_transcode = False
|
||||||
app.PLAYSTATE.resume_playback = None
|
|
||||||
|
|
||||||
|
|
||||||
def _playback_triage(plex_id, plex_type, path, resolve):
|
def _playback_triage(plex_id, plex_type, path, resolve, resume):
|
||||||
plex_id = utils.cast(int, plex_id)
|
plex_id = utils.cast(int, plex_id)
|
||||||
LOG.info('playback_triage called with plex_id %s, plex_type %s, path %s, '
|
LOG.debug('playback_triage called with plex_id %s, plex_type %s, path %s, '
|
||||||
'resolve %s', plex_id, plex_type, path, resolve)
|
'resolve %s, resume %s', plex_id, plex_type, path, resolve, resume)
|
||||||
global RESOLVE
|
global RESOLVE
|
||||||
# If started via Kodi context menu, we never resolve
|
# If started via Kodi context menu, we never resolve
|
||||||
RESOLVE = resolve if not app.PLAYSTATE.context_menu_play else False
|
RESOLVE = resolve if not app.PLAYSTATE.context_menu_play else False
|
||||||
|
@ -85,12 +85,12 @@ def _playback_triage(plex_id, plex_type, path, resolve):
|
||||||
except KeyError:
|
except KeyError:
|
||||||
# Kodi bug - Playlist plays (not Playqueue) will ALWAYS be audio for
|
# Kodi bug - Playlist plays (not Playqueue) will ALWAYS be audio for
|
||||||
# add-on paths
|
# add-on paths
|
||||||
LOG.info('No position returned from player! Assuming playlist')
|
LOG.debug('No position returned from player! Assuming playlist')
|
||||||
playqueue = PQ.get_playqueue_from_type(v.KODI_PLAYLIST_TYPE_AUDIO)
|
playqueue = PQ.get_playqueue_from_type(v.KODI_PLAYLIST_TYPE_AUDIO)
|
||||||
try:
|
try:
|
||||||
pos = js.get_position(playqueue.playlistid)
|
pos = js.get_position(playqueue.playlistid)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
LOG.info('Assuming video instead of audio playlist playback')
|
LOG.debug('Assuming video instead of audio playlist playback')
|
||||||
playqueue = PQ.get_playqueue_from_type(v.KODI_PLAYLIST_TYPE_VIDEO)
|
playqueue = PQ.get_playqueue_from_type(v.KODI_PLAYLIST_TYPE_VIDEO)
|
||||||
try:
|
try:
|
||||||
pos = js.get_position(playqueue.playlistid)
|
pos = js.get_position(playqueue.playlistid)
|
||||||
|
@ -108,12 +108,12 @@ def _playback_triage(plex_id, plex_type, path, resolve):
|
||||||
try:
|
try:
|
||||||
item = items[pos]
|
item = items[pos]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
LOG.info('Could not apply playlist hack! Probably Widget playback')
|
LOG.debug('Could not apply playlist hack! Probably Widget playback')
|
||||||
else:
|
else:
|
||||||
if ('id' not in item and
|
if ('id' not in item and
|
||||||
item.get('type') == 'unknown' and item.get('title') == ''):
|
item.get('type') == 'unknown' and item.get('title') == ''):
|
||||||
LOG.info('Kodi playlist play detected')
|
LOG.debug('Kodi playlist play detected')
|
||||||
_playlist_playback(plex_id, plex_type)
|
_playlist_playback(plex_id)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Can return -1 (as in "no playlist")
|
# Can return -1 (as in "no playlist")
|
||||||
|
@ -127,22 +127,20 @@ def _playback_triage(plex_id, plex_type, path, resolve):
|
||||||
initiate = True
|
initiate = True
|
||||||
else:
|
else:
|
||||||
if item.plex_id != plex_id:
|
if item.plex_id != plex_id:
|
||||||
LOG.debug('Received new plex_id %s, expected %s',
|
LOG.debug('Received new plex_id%s, expected %s',
|
||||||
plex_id, item.plex_id)
|
plex_id, item.plex_id)
|
||||||
initiate = True
|
initiate = True
|
||||||
else:
|
else:
|
||||||
initiate = False
|
initiate = False
|
||||||
if not initiate and app.PLAYSTATE.resume_playback is not None:
|
|
||||||
LOG.debug('Detected re-playing of the same item')
|
|
||||||
initiate = True
|
|
||||||
if initiate:
|
if initiate:
|
||||||
_playback_init(plex_id, plex_type, playqueue, pos)
|
_playback_init(plex_id, plex_type, playqueue, pos, resume)
|
||||||
else:
|
else:
|
||||||
# kick off playback on second pass
|
# kick off playback on second pass, resume was already set on first
|
||||||
|
# pass (threaded_playback will seek to resume)
|
||||||
_conclude_playback(playqueue, pos)
|
_conclude_playback(playqueue, pos)
|
||||||
|
|
||||||
|
|
||||||
def _playlist_playback(plex_id, plex_type):
|
def _playlist_playback(plex_id):
|
||||||
"""
|
"""
|
||||||
Really annoying Kodi behavior: Kodi will throw the ENTIRE playlist some-
|
Really annoying Kodi behavior: Kodi will throw the ENTIRE playlist some-
|
||||||
where, causing Playlist.onAdd to fire for each item like this:
|
where, causing Playlist.onAdd to fire for each item like this:
|
||||||
|
@ -175,11 +173,11 @@ def _playlist_playback(plex_id, plex_type):
|
||||||
_conclude_playback(playqueue, pos=0)
|
_conclude_playback(playqueue, pos=0)
|
||||||
|
|
||||||
|
|
||||||
def _playback_init(plex_id, plex_type, playqueue, pos):
|
def _playback_init(plex_id, plex_type, playqueue, pos, resume):
|
||||||
"""
|
"""
|
||||||
Playback setup if Kodi starts playing an item for the first time.
|
Playback setup if Kodi starts playing an item for the first time.
|
||||||
"""
|
"""
|
||||||
LOG.info('Initializing PKC playback')
|
LOG.debug('Initializing PKC playback')
|
||||||
# Stop playback so we don't get an error message that the last item of the
|
# Stop playback so we don't get an error message that the last item of the
|
||||||
# queue failed to play
|
# queue failed to play
|
||||||
app.APP.player.stop()
|
app.APP.player.stop()
|
||||||
|
@ -211,18 +209,17 @@ def _playback_init(plex_id, plex_type, playqueue, pos):
|
||||||
# Release default.py
|
# Release default.py
|
||||||
_ensure_resolve()
|
_ensure_resolve()
|
||||||
api = API(xml[0])
|
api = API(xml[0])
|
||||||
if api.resume_point() and (app.SYNC.direct_paths or
|
if (app.PLAYSTATE.context_menu_play and
|
||||||
app.PLAYSTATE.context_menu_play):
|
api.resume_point() and
|
||||||
# Since Kodi won't ask if user wants to resume playback -
|
api.plex_type in v.PLEX_VIDEOTYPES):
|
||||||
# we need to ask ourselves
|
# User chose to either play via PMS or to force transcode
|
||||||
|
# Need to prompt whether we should resume_playback
|
||||||
resume = resume_dialog(int(api.resume_point()))
|
resume = resume_dialog(int(api.resume_point()))
|
||||||
if resume is None:
|
if resume is None:
|
||||||
LOG.info('User cancelled resume dialog')
|
# User cancelled dialog
|
||||||
return
|
return
|
||||||
elif app.SYNC.direct_paths:
|
LOG.debug('Using resume %s', resume)
|
||||||
resume = False
|
resume = resume or False
|
||||||
else:
|
|
||||||
resume = app.PLAYSTATE.resume_playback or False
|
|
||||||
trailers = False
|
trailers = False
|
||||||
if (not resume and plex_type == v.PLEX_TYPE_MOVIE and
|
if (not resume and plex_type == v.PLEX_TYPE_MOVIE and
|
||||||
utils.settings('enableCinema') == "true"):
|
utils.settings('enableCinema') == "true"):
|
||||||
|
@ -251,11 +248,17 @@ def _playback_init(plex_id, plex_type, playqueue, pos):
|
||||||
PL.get_playlist_details_from_xml(playqueue, xml)
|
PL.get_playlist_details_from_xml(playqueue, xml)
|
||||||
stack = _prep_playlist_stack(xml, resume)
|
stack = _prep_playlist_stack(xml, resume)
|
||||||
_process_stack(playqueue, stack)
|
_process_stack(playqueue, stack)
|
||||||
|
offset = _use_kodi_db_offset(playqueue.items[pos].plex_id,
|
||||||
|
playqueue.items[pos].plex_type,
|
||||||
|
playqueue.items[pos].offset) if resume else 0
|
||||||
# New thread to release this one sooner (e.g. harddisk spinning up)
|
# New thread to release this one sooner (e.g. harddisk spinning up)
|
||||||
thread = Thread(target=threaded_playback,
|
thread = Thread(target=threaded_playback,
|
||||||
args=(playqueue.kodi_pl, pos, None))
|
args=(playqueue.kodi_pl, pos, offset))
|
||||||
thread.setDaemon(True)
|
thread.setDaemon(True)
|
||||||
LOG.info('Done initializing playback, starting Kodi player at pos %s', pos)
|
LOG.debug('Done initializing playback, starting Kodi player at pos %s and '
|
||||||
|
'offset %s', pos, offset)
|
||||||
|
# Ensure that PKC playqueue monitor ignores the changes we just made
|
||||||
|
playqueue.pkc_edit = True
|
||||||
# By design, PKC will start Kodi playback using Player().play(). Kodi
|
# By design, PKC will start Kodi playback using Player().play(). Kodi
|
||||||
# caches paths like our plugin://pkc. If we use Player().play() between
|
# caches paths like our plugin://pkc. If we use Player().play() between
|
||||||
# 2 consecutive startups of exactly the same Kodi library item, Kodi's
|
# 2 consecutive startups of exactly the same Kodi library item, Kodi's
|
||||||
|
@ -263,8 +266,6 @@ def _playback_init(plex_id, plex_type, playqueue, pos):
|
||||||
# plugin://pkc will be lost; Kodi will try to startup playback for an empty
|
# plugin://pkc will be lost; Kodi will try to startup playback for an empty
|
||||||
# path: log entry is "CGUIWindowVideoBase::OnPlayMedia <missing path>"
|
# path: log entry is "CGUIWindowVideoBase::OnPlayMedia <missing path>"
|
||||||
thread.start()
|
thread.start()
|
||||||
# Ensure that PKC playqueue monitor ignores the changes we just made
|
|
||||||
playqueue.pkc_edit = True
|
|
||||||
|
|
||||||
|
|
||||||
def _ensure_resolve(abort=False):
|
def _ensure_resolve(abort=False):
|
||||||
|
@ -297,6 +298,7 @@ def resume_dialog(resume):
|
||||||
# "Resume from {0:s}"
|
# "Resume from {0:s}"
|
||||||
# "Start from beginning"
|
# "Start from beginning"
|
||||||
resume = datetime.timedelta(seconds=resume)
|
resume = datetime.timedelta(seconds=resume)
|
||||||
|
LOG.debug('Showing PKC resume dialog for resume: %s', resume)
|
||||||
answ = utils.dialog('contextmenu',
|
answ = utils.dialog('contextmenu',
|
||||||
[utils.lang(12022).replace('{0:s}', '{0}').format(unicode(resume)),
|
[utils.lang(12022).replace('{0:s}', '{0}').format(unicode(resume)),
|
||||||
utils.lang(12021)])
|
utils.lang(12021)])
|
||||||
|
@ -376,7 +378,7 @@ def _prep_playlist_stack(xml, resume):
|
||||||
'part': part,
|
'part': part,
|
||||||
'playcount': api.viewcount(),
|
'playcount': api.viewcount(),
|
||||||
'offset': api.resume_point(),
|
'offset': api.resume_point(),
|
||||||
'resume': resume if i + 1 == len(xml) and part == 0 else False,
|
'resume': resume if part == 0 and i + 1 == len(xml) else None,
|
||||||
'id': api.item_id()
|
'id': api.item_id()
|
||||||
})
|
})
|
||||||
return stack
|
return stack
|
||||||
|
@ -413,33 +415,20 @@ def _process_stack(playqueue, stack):
|
||||||
pos += 1
|
pos += 1
|
||||||
|
|
||||||
|
|
||||||
def _set_resume(listitem, item, api):
|
def _use_kodi_db_offset(plex_id, plex_type, plex_offset):
|
||||||
if item.plex_type in (v.PLEX_TYPE_SONG, v.PLEX_TYPE_CLIP):
|
"""
|
||||||
return
|
Do NOT use item.offset directly but get it from the Kodi DB (Plex might not
|
||||||
if item.resume is True:
|
have gotten the last resume point)
|
||||||
# Do NOT use item.offset directly but get it from the DB
|
"""
|
||||||
# (user might have initiated same video twice)
|
if plex_type not in (v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_EPISODE):
|
||||||
with PlexDB(lock=False) as plexdb:
|
return plex_offset
|
||||||
db_item = plexdb.item_by_id(item.plex_id, item.plex_type)
|
with PlexDB(lock=False) as plexdb:
|
||||||
if db_item:
|
db_item = plexdb.item_by_id(plex_id, plex_type)
|
||||||
file_id = db_item['kodi_fileid']
|
if db_item:
|
||||||
with KodiVideoDB(lock=False) as kodidb:
|
with KodiVideoDB(lock=False) as kodidb:
|
||||||
item.offset = kodidb.get_resume(file_id)
|
return kodidb.get_resume(db_item['kodi_fileid'])
|
||||||
LOG.info('Resuming playback at %s', item.offset)
|
else:
|
||||||
if v.KODIVERSION >= 18 and api:
|
return plex_offset
|
||||||
# Kodi 18 Alpha 3 broke StartOffset
|
|
||||||
try:
|
|
||||||
percent = (item.offset or api.resume_point()) / api.runtime() * 100.0
|
|
||||||
except ZeroDivisionError:
|
|
||||||
percent = 0.0
|
|
||||||
LOG.debug('Resuming at %s percent', percent)
|
|
||||||
listitem.setProperty('StartPercent', str(percent))
|
|
||||||
else:
|
|
||||||
listitem.setProperty('StartOffset', str(item.offset))
|
|
||||||
listitem.setProperty('resumetime', str(item.offset))
|
|
||||||
elif v.KODIVERSION >= 18:
|
|
||||||
# Make sure that the video starts from the beginning
|
|
||||||
listitem.setProperty('StartPercent', '0')
|
|
||||||
|
|
||||||
|
|
||||||
def _conclude_playback(playqueue, pos):
|
def _conclude_playback(playqueue, pos):
|
||||||
|
@ -457,19 +446,14 @@ def _conclude_playback(playqueue, pos):
|
||||||
start playback
|
start playback
|
||||||
return PKC listitem attached to result
|
return PKC listitem attached to result
|
||||||
"""
|
"""
|
||||||
LOG.info('Concluding playback for playqueue position %s', pos)
|
LOG.debug('Concluding playback for playqueue position %s', pos)
|
||||||
item = playqueue.items[pos]
|
item = playqueue.items[pos]
|
||||||
if item.xml is not None:
|
api = API(item.xml)
|
||||||
# Got a Plex element
|
api.part = item.part or 0
|
||||||
api = API(item.xml)
|
listitem = api.listitem(listitem=transfer.PKCListItem, resume=False)
|
||||||
api.part = item.part or 0
|
set_playurl(api, item)
|
||||||
listitem = api.listitem(listitem=transfer.PKCListItem)
|
|
||||||
set_playurl(api, item)
|
|
||||||
else:
|
|
||||||
listitem = transfer.PKCListItem()
|
|
||||||
api = None
|
|
||||||
if not item.file:
|
if not item.file:
|
||||||
LOG.info('Did not get a playurl, aborting playback silently')
|
LOG.debug('Did not get a playurl, aborting playback silently')
|
||||||
_ensure_resolve()
|
_ensure_resolve()
|
||||||
return
|
return
|
||||||
listitem.setPath(item.file.encode('utf-8'))
|
listitem.setPath(item.file.encode('utf-8'))
|
||||||
|
@ -478,9 +462,8 @@ def _conclude_playback(playqueue, pos):
|
||||||
elif item.playmethod in (v.PLAYBACK_METHOD_DIRECT_STREAM,
|
elif item.playmethod in (v.PLAYBACK_METHOD_DIRECT_STREAM,
|
||||||
v.PLAYBACK_METHOD_TRANSCODE):
|
v.PLAYBACK_METHOD_TRANSCODE):
|
||||||
audio_subtitle_prefs(api, listitem)
|
audio_subtitle_prefs(api, listitem)
|
||||||
_set_resume(listitem, item, api)
|
|
||||||
transfer.send(listitem)
|
transfer.send(listitem)
|
||||||
LOG.info('Done concluding playback')
|
LOG.debug('Done concluding playback')
|
||||||
|
|
||||||
|
|
||||||
def process_indirect(key, offset, resolve=True):
|
def process_indirect(key, offset, resolve=True):
|
||||||
|
@ -494,8 +477,8 @@ def process_indirect(key, offset, resolve=True):
|
||||||
Set resolve to False if playback should be kicked off directly, not via
|
Set resolve to False if playback should be kicked off directly, not via
|
||||||
setResolvedUrl
|
setResolvedUrl
|
||||||
"""
|
"""
|
||||||
LOG.info('process_indirect called with key: %s, offset: %s, resolve: %s',
|
LOG.debug('process_indirect called with key: %s, offset: %s, resolve: %s',
|
||||||
key, offset, resolve)
|
key, offset, resolve)
|
||||||
global RESOLVE
|
global RESOLVE
|
||||||
RESOLVE = resolve
|
RESOLVE = resolve
|
||||||
offset = int(v.PLEX_TO_KODI_TIMEFACTOR * float(offset)) if offset != '0' else None
|
offset = int(v.PLEX_TO_KODI_TIMEFACTOR * float(offset)) if offset != '0' else None
|
||||||
|
@ -513,7 +496,7 @@ def process_indirect(key, offset, resolve=True):
|
||||||
return
|
return
|
||||||
|
|
||||||
api = API(xml[0])
|
api = API(xml[0])
|
||||||
listitem = api.listitem(listitem=transfer.PKCListItem)
|
listitem = api.listitem(listitem=transfer.PKCListItem, resume=False)
|
||||||
playqueue = PQ.get_playqueue_from_type(
|
playqueue = PQ.get_playqueue_from_type(
|
||||||
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type])
|
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type])
|
||||||
playqueue.clear()
|
playqueue.clear()
|
||||||
|
@ -552,7 +535,7 @@ def process_indirect(key, offset, resolve=True):
|
||||||
args={'item': utils.try_encode(playurl),
|
args={'item': utils.try_encode(playurl),
|
||||||
'listitem': listitem})
|
'listitem': listitem})
|
||||||
thread.setDaemon(True)
|
thread.setDaemon(True)
|
||||||
LOG.info('Done initializing PKC playback, starting Kodi player')
|
LOG.debug('Done initializing PKC playback, starting Kodi player')
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
|
|
||||||
|
@ -563,9 +546,9 @@ def play_xml(playqueue, xml, offset=None, start_plex_id=None):
|
||||||
Either supply the ratingKey of the starting Plex element. Or set
|
Either supply the ratingKey of the starting Plex element. Or set
|
||||||
playqueue.selectedItemID
|
playqueue.selectedItemID
|
||||||
"""
|
"""
|
||||||
offset = int(offset) if offset else None
|
offset = int(offset) / 1000 if offset else None
|
||||||
LOG.info("play_xml called with offset %s, start_plex_id %s",
|
LOG.debug("play_xml called with offset %s, start_plex_id %s",
|
||||||
offset, start_plex_id)
|
offset, start_plex_id)
|
||||||
start_item = start_plex_id if start_plex_id is not None \
|
start_item = start_plex_id if start_plex_id is not None \
|
||||||
else playqueue.selectedItemID
|
else playqueue.selectedItemID
|
||||||
for startpos, video in enumerate(xml):
|
for startpos, video in enumerate(xml):
|
||||||
|
@ -581,21 +564,40 @@ def play_xml(playqueue, xml, offset=None, start_plex_id=None):
|
||||||
LOG.debug('Playqueue after play_xml update: %s', playqueue)
|
LOG.debug('Playqueue after play_xml update: %s', playqueue)
|
||||||
thread = Thread(target=threaded_playback,
|
thread = Thread(target=threaded_playback,
|
||||||
args=(playqueue.kodi_pl, startpos, offset))
|
args=(playqueue.kodi_pl, startpos, offset))
|
||||||
LOG.info('Done play_xml, starting Kodi player at position %s', startpos)
|
LOG.debug('Done play_xml, starting Kodi player at position %s', startpos)
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
|
|
||||||
def threaded_playback(kodi_playlist, startpos, offset):
|
def threaded_playback(kodi_playlist, startpos, offset):
|
||||||
"""
|
"""
|
||||||
Seek immediately after kicking off playback is not reliable.
|
Seek immediately after kicking off playback is not reliable. We even seek
|
||||||
|
to 0 (starting position) in case Kodi wants to resume but we want to start
|
||||||
|
over.
|
||||||
|
|
||||||
|
offset: resume position in seconds [int/float]
|
||||||
"""
|
"""
|
||||||
|
LOG.debug('threaded_playback with startpos %s, offset %s',
|
||||||
|
startpos, offset)
|
||||||
app.APP.player.play(kodi_playlist, None, False, startpos)
|
app.APP.player.play(kodi_playlist, None, False, startpos)
|
||||||
if offset and offset != '0':
|
offset = offset if offset else 0
|
||||||
i = 0
|
i = 0
|
||||||
while not app.APP.is_playing or not js.get_player_ids():
|
while not app.APP.is_playing or not js.get_player_ids():
|
||||||
app.APP.monitor.waitForAbort(0.1)
|
if app.APP.monitor.waitForAbort(0.1):
|
||||||
i += 1
|
# PKC needs to quit
|
||||||
if i > 100:
|
return
|
||||||
LOG.error('Could not seek to %s', offset)
|
i += 1
|
||||||
return
|
if i > 200:
|
||||||
js.seek_to(int(offset))
|
LOG.error('Could not seek to %s', offset)
|
||||||
|
return
|
||||||
|
i = 0
|
||||||
|
answ = js.seek_to(offset * 1000)
|
||||||
|
while 'error' in answ:
|
||||||
|
# Kodi sometimes returns {u'message': u'Failed to execute method.',
|
||||||
|
# u'code': -32100} if user quickly switches videos
|
||||||
|
i += 1
|
||||||
|
if i > 10:
|
||||||
|
LOG.error('Failed to seek to %s', offset)
|
||||||
|
return
|
||||||
|
app.APP.monitor.waitForAbort(0.1)
|
||||||
|
answ = js.seek_to(offset * 1000)
|
||||||
|
LOG.debug('Seek to offset %s successful', offset)
|
||||||
|
|
|
@ -6,7 +6,7 @@ from requests import exceptions
|
||||||
|
|
||||||
from .downloadutils import DownloadUtils as DU
|
from .downloadutils import DownloadUtils as DU
|
||||||
from .plex_api import API
|
from .plex_api import API
|
||||||
from . import plex_functions as PF, utils, app, variables as v
|
from . import plex_functions as PF, utils, variables as v
|
||||||
|
|
||||||
|
|
||||||
LOG = getLogger('PLEX.playback_decision')
|
LOG = getLogger('PLEX.playback_decision')
|
||||||
|
|
|
@ -37,10 +37,15 @@ class PlaybackTask(backgroundthread.Task):
|
||||||
resolve = False if params.get('handle') == '-1' else True
|
resolve = False if params.get('handle') == '-1' else True
|
||||||
LOG.debug('Received mode: %s, params: %s', mode, params)
|
LOG.debug('Received mode: %s, params: %s', mode, params)
|
||||||
if mode == 'play':
|
if mode == 'play':
|
||||||
|
if params.get('resume'):
|
||||||
|
resume = params.get('resume') == '1'
|
||||||
|
else:
|
||||||
|
resume = None
|
||||||
playback.playback_triage(plex_id=params.get('plex_id'),
|
playback.playback_triage(plex_id=params.get('plex_id'),
|
||||||
plex_type=params.get('plex_type'),
|
plex_type=params.get('plex_type'),
|
||||||
path=params.get('path'),
|
path=params.get('path'),
|
||||||
resolve=resolve)
|
resolve=resolve,
|
||||||
|
resume=resume)
|
||||||
elif mode == 'plex_node':
|
elif mode == 'plex_node':
|
||||||
playback.process_indirect(params['key'],
|
playback.process_indirect(params['key'],
|
||||||
params['offset'],
|
params['offset'],
|
||||||
|
|
|
@ -213,6 +213,7 @@ class PlaylistItem(object):
|
||||||
"'guid': '{self.guid}', "
|
"'guid': '{self.guid}', "
|
||||||
"'playmethod': '{self.playmethod}', "
|
"'playmethod': '{self.playmethod}', "
|
||||||
"'playcount': {self.playcount}, "
|
"'playcount': {self.playcount}, "
|
||||||
|
"'resume': {self.resume},"
|
||||||
"'offset': {self.offset}, "
|
"'offset': {self.offset}, "
|
||||||
"'force_transcode': {self.force_transcode}, "
|
"'force_transcode': {self.force_transcode}, "
|
||||||
"'part': {self.part}".format(self=self))
|
"'part': {self.part}".format(self=self))
|
||||||
|
|
|
@ -64,7 +64,11 @@ def _m3u_iterator(text):
|
||||||
lines = iter(text.split('\n'))
|
lines = iter(text.split('\n'))
|
||||||
for line in lines:
|
for line in lines:
|
||||||
if line.startswith('#EXTINF:'):
|
if line.startswith('#EXTINF:'):
|
||||||
yield next(lines).strip()
|
next_line = next(lines).strip()
|
||||||
|
if next_line.startswith('#EXT-KX-OFFSET:'):
|
||||||
|
yield next(lines).strip()
|
||||||
|
else:
|
||||||
|
yield next_line
|
||||||
|
|
||||||
|
|
||||||
def m3u_to_plex_ids(playlist):
|
def m3u_to_plex_ids(playlist):
|
||||||
|
|
|
@ -57,6 +57,12 @@ class Base(object):
|
||||||
"""
|
"""
|
||||||
return self.xml.tag
|
return self.xml.tag
|
||||||
|
|
||||||
|
def tag_label(self):
|
||||||
|
"""
|
||||||
|
Returns the 'tag' attribute of the xml
|
||||||
|
"""
|
||||||
|
return self.xml.get('tag')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def attrib(self):
|
def attrib(self):
|
||||||
"""
|
"""
|
||||||
|
@ -605,11 +611,16 @@ class Base(object):
|
||||||
% (v.ADDON_ID, url, v.PLEX_TYPE_CLIP))
|
% (v.ADDON_ID, url, v.PLEX_TYPE_CLIP))
|
||||||
return url
|
return url
|
||||||
|
|
||||||
def listitem(self, listitem=xbmcgui.ListItem):
|
def listitem(self, listitem=xbmcgui.ListItem, resume=True):
|
||||||
"""
|
"""
|
||||||
Returns a xbmcgui.ListItem() (or PKCListItem) for this Plex element
|
Returns a xbmcgui.ListItem() (or PKCListItem) for this Plex element
|
||||||
|
|
||||||
|
Pass resume=False in order to NOT set a resume point (but let Kodi
|
||||||
|
automatically handle it)
|
||||||
"""
|
"""
|
||||||
item = widgets.generate_item(self)
|
item = widgets.generate_item(self)
|
||||||
|
if not resume and 'resume' in item:
|
||||||
|
del item['resume']
|
||||||
item = widgets.prepare_listitem(item)
|
item = widgets.prepare_listitem(item)
|
||||||
return widgets.create_listitem(item, as_tuple=False, listitem=listitem)
|
return widgets.create_listitem(item, as_tuple=False, listitem=listitem)
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import absolute_import, division, unicode_literals
|
from __future__ import absolute_import, division, unicode_literals
|
||||||
|
|
||||||
from ..utils import cast
|
|
||||||
from .. import utils, variables as v, app
|
from .. import utils, variables as v, app
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -59,10 +59,8 @@ def update_playqueue_from_PMS(playqueue,
|
||||||
# reconnects and Kodi is already playing something - silly, really
|
# reconnects and Kodi is already playing something - silly, really
|
||||||
# For all other cases, a new playqueue is generated by Plex
|
# For all other cases, a new playqueue is generated by Plex
|
||||||
LOG.debug('Update for existing playqueue detected')
|
LOG.debug('Update for existing playqueue detected')
|
||||||
new = False
|
return
|
||||||
else:
|
playqueue.clear()
|
||||||
new = True
|
|
||||||
playqueue.clear()
|
|
||||||
# Get new metadata for the playqueue first
|
# Get new metadata for the playqueue first
|
||||||
try:
|
try:
|
||||||
PL.get_playlist_details_from_xml(playqueue, xml)
|
PL.get_playlist_details_from_xml(playqueue, xml)
|
||||||
|
@ -71,33 +69,10 @@ def update_playqueue_from_PMS(playqueue,
|
||||||
return
|
return
|
||||||
playqueue.repeat = 0 if not repeat else int(repeat)
|
playqueue.repeat = 0 if not repeat else int(repeat)
|
||||||
playqueue.plex_transient_token = transient_token
|
playqueue.plex_transient_token = transient_token
|
||||||
if new:
|
playback.play_xml(playqueue,
|
||||||
playback.play_xml(playqueue,
|
xml,
|
||||||
xml,
|
offset=offset,
|
||||||
offset=offset,
|
start_plex_id=start_plex_id)
|
||||||
start_plex_id=start_plex_id)
|
|
||||||
return
|
|
||||||
# Updates to playqueues could potentially become a bit more ugly...
|
|
||||||
if app.APP.is_playing:
|
|
||||||
try:
|
|
||||||
playerid = js.get_player_ids()[0]
|
|
||||||
except IndexError:
|
|
||||||
LOG.error('Unexpectately could not get Kodi player id')
|
|
||||||
return
|
|
||||||
if app.PLAYSTATE.player_states[playerid]['plex_id'] == start_plex_id:
|
|
||||||
# Nothing to do - let's not seek to avoid jumps in playback
|
|
||||||
return
|
|
||||||
pos = playqueue.position_from_plex_id(start_plex_id)
|
|
||||||
LOG.debug('Skipping to position %s for %s', pos, playqueue)
|
|
||||||
js.skipto(pos)
|
|
||||||
if offset:
|
|
||||||
js.seek_to(offset)
|
|
||||||
return
|
|
||||||
# Need to initiate playback again using our existing playqueue
|
|
||||||
app.APP.player.play(playqueue.kodi_pl,
|
|
||||||
None,
|
|
||||||
False,
|
|
||||||
playqueue.position_from_plex_id(start_plex_id))
|
|
||||||
|
|
||||||
|
|
||||||
class PlexCompanion(backgroundthread.KillableThread):
|
class PlexCompanion(backgroundthread.KillableThread):
|
||||||
|
@ -151,11 +126,10 @@ class PlexCompanion(backgroundthread.KillableThread):
|
||||||
playback.play_xml(playqueue, xml, offset)
|
playback.play_xml(playqueue, xml, offset)
|
||||||
else:
|
else:
|
||||||
app.CONN.plex_transient_token = data.get('token')
|
app.CONN.plex_transient_token = data.get('token')
|
||||||
if data.get('offset') != '0':
|
|
||||||
app.PLAYSTATE.resume_playback = True
|
|
||||||
playback.playback_triage(api.plex_id,
|
playback.playback_triage(api.plex_id,
|
||||||
api.plex_type,
|
api.plex_type,
|
||||||
resolve=False)
|
resolve=False,
|
||||||
|
resume=data.get('offset') not in ('0', None))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _process_node(data):
|
def _process_node(data):
|
||||||
|
|
|
@ -95,7 +95,6 @@ class Service(object):
|
||||||
self.setup = None
|
self.setup = None
|
||||||
self.alexa = None
|
self.alexa = None
|
||||||
self.playqueue = None
|
self.playqueue = None
|
||||||
self.context_monitor = None
|
|
||||||
# Flags for other threads
|
# Flags for other threads
|
||||||
self.connection_check_running = False
|
self.connection_check_running = False
|
||||||
self.auth_running = False
|
self.auth_running = False
|
||||||
|
@ -422,9 +421,6 @@ class Service(object):
|
||||||
# Some plumbing
|
# Some plumbing
|
||||||
app.init()
|
app.init()
|
||||||
app.APP.monitor = kodimonitor.KodiMonitor()
|
app.APP.monitor = kodimonitor.KodiMonitor()
|
||||||
self.context_monitor = kodimonitor.ContextMonitor()
|
|
||||||
# Start immediately to catch user input even before auth
|
|
||||||
self.context_monitor.start()
|
|
||||||
app.APP.player = xbmc.Player()
|
app.APP.player = xbmc.Player()
|
||||||
# Initialize the PKC playqueues
|
# Initialize the PKC playqueues
|
||||||
PQ.init_playqueues()
|
PQ.init_playqueues()
|
||||||
|
|
|
@ -63,25 +63,21 @@ def kodi_now():
|
||||||
|
|
||||||
def millis_to_kodi_time(milliseconds):
|
def millis_to_kodi_time(milliseconds):
|
||||||
"""
|
"""
|
||||||
Converts time in milliseconds to the time dict used by the Kodi JSON RPC:
|
Converts time in milliseconds [int or float] to the time dict used by the
|
||||||
|
Kodi JSON RPC:
|
||||||
{
|
{
|
||||||
'hours': [int],
|
'hours': [int],
|
||||||
'minutes': [int],
|
'minutes': [int],
|
||||||
'seconds'[int],
|
'seconds'[int],
|
||||||
'milliseconds': [int]
|
'milliseconds': [int]
|
||||||
}
|
}
|
||||||
Pass in the time in milliseconds as an int
|
|
||||||
"""
|
"""
|
||||||
seconds = int(milliseconds / 1000)
|
seconds = int(milliseconds / 1000)
|
||||||
minutes = int(seconds / 60)
|
minutes = int(seconds / 60)
|
||||||
seconds = seconds % 60
|
return {'hours': int(minutes / 60),
|
||||||
hours = int(minutes / 60)
|
'minutes': int(minutes % 60),
|
||||||
minutes = minutes % 60
|
'seconds': int(seconds % 60),
|
||||||
milliseconds = milliseconds % 1000
|
'milliseconds': int(milliseconds % 1000)}
|
||||||
return {'hours': hours,
|
|
||||||
'minutes': minutes,
|
|
||||||
'seconds': seconds,
|
|
||||||
'milliseconds': milliseconds}
|
|
||||||
|
|
||||||
|
|
||||||
def kodi_time_to_millis(time):
|
def kodi_time_to_millis(time):
|
||||||
|
|
|
@ -181,6 +181,9 @@ PLEX_TYPE_PHOTO = 'photo'
|
||||||
PLEX_TYPE_PLAYLIST = 'playlist'
|
PLEX_TYPE_PLAYLIST = 'playlist'
|
||||||
PLEX_TYPE_CHANNEL = 'channel'
|
PLEX_TYPE_CHANNEL = 'channel'
|
||||||
|
|
||||||
|
# E.g. PMS answer when hitting the PMS endpoint /hubs/search
|
||||||
|
PLEX_TYPE_TAG = 'tag'
|
||||||
|
|
||||||
# Used for /:/timeline XML messages
|
# Used for /:/timeline XML messages
|
||||||
PLEX_PLAYLIST_TYPE_VIDEO = 'video'
|
PLEX_PLAYLIST_TYPE_VIDEO = 'video'
|
||||||
PLEX_PLAYLIST_TYPE_AUDIO = 'music'
|
PLEX_PLAYLIST_TYPE_AUDIO = 'music'
|
||||||
|
|
|
@ -105,9 +105,10 @@ def _generate_folder(api):
|
||||||
return content
|
return content
|
||||||
else:
|
else:
|
||||||
art = api.artwork()
|
art = api.artwork()
|
||||||
|
title = api.title() if api.plex_type != v.PLEX_TYPE_TAG else api.tag_label()
|
||||||
return {
|
return {
|
||||||
'title': api.title(),
|
'title': title,
|
||||||
'label': api.title(),
|
'label': title,
|
||||||
'file': api.directory_path(section_id=SECTION_ID,
|
'file': api.directory_path(section_id=SECTION_ID,
|
||||||
plex_type=PLEX_TYPE,
|
plex_type=PLEX_TYPE,
|
||||||
old_key=KEY),
|
old_key=KEY),
|
||||||
|
|
Loading…
Reference in a new issue