commit
00f69d8568
16 changed files with 235 additions and 110 deletions
|
@ -1,5 +1,5 @@
|
||||||
[![stable version](https://img.shields.io/badge/stable_version-2.7.14-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.8.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.7.14-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.8.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)
|
||||||
|
|
29
addon.xml
29
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.7.14" provider-name="croneter">
|
<addon id="plugin.video.plexkodiconnect" name="PlexKodiConnect" version="2.8.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.0.9" />
|
<import addon="plugin.video.plexkodiconnect.movies" version="2.1.1" />
|
||||||
<import addon="plugin.video.plexkodiconnect.tvshows" version="2.0.10" />
|
<import addon="plugin.video.plexkodiconnect.tvshows" version="2.1.1" />
|
||||||
</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>
|
||||||
|
@ -77,7 +77,28 @@
|
||||||
<summary lang="uk_UA">Нативна інтеграція Plex в Kodi</summary>
|
<summary lang="uk_UA">Нативна інтеграція Plex в Kodi</summary>
|
||||||
<description lang="uk_UA">Підключає Kodi до серверу Plex. Цей плагін передбачає, що ви керуєте всіма своїми відео за допомогою Plex (і ніяк не Kodi). Ви можете втратити дані, які вже зберігаються у відео та музичних БД Kodi (оскільки цей плагін безпосередньо їх змінює). Використовуйте на свій страх і ризик!</description>
|
<description lang="uk_UA">Підключає Kodi до серверу Plex. Цей плагін передбачає, що ви керуєте всіма своїми відео за допомогою Plex (і ніяк не Kodi). Ви можете втратити дані, які вже зберігаються у відео та музичних БД Kodi (оскільки цей плагін безпосередньо їх змінює). Використовуйте на свій страх і ризик!</description>
|
||||||
<disclaimer lang="uk_UA">Використовуйте на свій ризик</disclaimer>
|
<disclaimer lang="uk_UA">Використовуйте на свій ризик</disclaimer>
|
||||||
<news>version 2.7.14:
|
<news>version 2.8.0:
|
||||||
|
- Finally fix Kodi crashing on playback startup for add-on paths!
|
||||||
|
- All the good stuff from 2.7.15-2.7.18 for everyone
|
||||||
|
|
||||||
|
version 2.7.18 (beta only):
|
||||||
|
- Fix Kodi always playing the same file version of a video if several are present
|
||||||
|
- Also play trailers if user chose to resume movie from the beginning
|
||||||
|
- Ask user whether to resume if using Direct Paths and user initiated playback via PMS
|
||||||
|
- Fix video thrown by Plex Companion not resuming
|
||||||
|
|
||||||
|
version 2.7.17 (beta only):
|
||||||
|
- Another attempt to keep Kodi from crashing on playback startup
|
||||||
|
|
||||||
|
version 2.7.16 (beta only):
|
||||||
|
- Hopefully fix Kodi crashing on playback startup for good
|
||||||
|
|
||||||
|
version 2.7.15 (beta only):
|
||||||
|
- Hopefully fix Kodi crashing on playback startup
|
||||||
|
- Refresh widgets only on homescreen to prevent cursor from jumping within libraries
|
||||||
|
- Don't refresh container when user chose to delete or refresh an item from the context menu
|
||||||
|
|
||||||
|
version 2.7.14:
|
||||||
- Correctly clear window variables e.g. on user switch
|
- Correctly clear window variables e.g. on user switch
|
||||||
- Reload skin on resetting PKC video nodes
|
- Reload skin on resetting PKC video nodes
|
||||||
- Fix last-played node value to ensure a playcount greater than zero
|
- Fix last-played node value to ensure a playcount greater than zero
|
||||||
|
|
|
@ -1,3 +1,24 @@
|
||||||
|
version 2.8.0:
|
||||||
|
- Finally fix Kodi crashing on playback startup for add-on paths!
|
||||||
|
- All the good stuff from 2.7.15-2.7.18 for everyone
|
||||||
|
|
||||||
|
version 2.7.18 (beta only):
|
||||||
|
- Fix Kodi always playing the same file version of a video if several are present
|
||||||
|
- Also play trailers if user chose to resume movie from the beginning
|
||||||
|
- Ask user whether to resume if using Direct Paths and user initiated playback via PMS
|
||||||
|
- Fix video thrown by Plex Companion not resuming
|
||||||
|
|
||||||
|
version 2.7.17 (beta only):
|
||||||
|
- Another attempt to keep Kodi from crashing on playback startup
|
||||||
|
|
||||||
|
version 2.7.16 (beta only):
|
||||||
|
- Hopefully fix Kodi crashing on playback startup for good
|
||||||
|
|
||||||
|
version 2.7.15 (beta only):
|
||||||
|
- Hopefully fix Kodi crashing on playback startup
|
||||||
|
- Refresh widgets only on homescreen to prevent cursor from jumping within libraries
|
||||||
|
- Don't refresh container when user chose to delete or refresh an item from the context menu
|
||||||
|
|
||||||
version 2.7.14:
|
version 2.7.14:
|
||||||
- Correctly clear window variables e.g. on user switch
|
- Correctly clear window variables e.g. on user switch
|
||||||
- Reload skin on resetting PKC video nodes
|
- Reload skin on resetting PKC video nodes
|
||||||
|
|
14
default.py
14
default.py
|
@ -157,17 +157,11 @@ class Main():
|
||||||
# Handle -1 received, not waiting for main thread
|
# Handle -1 received, not waiting for main thread
|
||||||
return
|
return
|
||||||
# Wait for the result from the main PKC thread
|
# Wait for the result from the main PKC thread
|
||||||
result = transfer.wait_for_transfer()
|
result = transfer.wait_for_transfer(source='main')
|
||||||
if result is None:
|
if result is True:
|
||||||
LOG.error('Error encountered, aborting')
|
|
||||||
utils.dialog('notification',
|
|
||||||
heading='{plex}',
|
|
||||||
message=utils.lang(30128),
|
|
||||||
icon='{error}',
|
|
||||||
time=3000)
|
|
||||||
xbmcplugin.setResolvedUrl(HANDLE, False, xbmcgui.ListItem())
|
xbmcplugin.setResolvedUrl(HANDLE, False, xbmcgui.ListItem())
|
||||||
elif result is True:
|
# Tell main thread that we're done
|
||||||
pass
|
transfer.send(True, target='main')
|
||||||
else:
|
else:
|
||||||
# Received a xbmcgui.ListItem()
|
# Received a xbmcgui.ListItem()
|
||||||
xbmcplugin.setResolvedUrl(HANDLE, True, result)
|
xbmcplugin.setResolvedUrl(HANDLE, True, result)
|
||||||
|
|
|
@ -27,6 +27,8 @@ class App(object):
|
||||||
self.stop_pkc = False
|
self.stop_pkc = False
|
||||||
# This will suspend the main thread also
|
# This will suspend the main thread also
|
||||||
self.suspend = False
|
self.suspend = False
|
||||||
|
# Update Kodi widgets
|
||||||
|
self.update_widgets = False
|
||||||
# Need to lock all methods and functions messing with Plex Companion subscribers
|
# Need to lock all methods and functions messing with Plex Companion subscribers
|
||||||
self.lock_subscriber = RLock()
|
self.lock_subscriber = RLock()
|
||||||
# Need to lock everything messing with Kodi/PKC playqueues
|
# Need to lock everything messing with Kodi/PKC playqueues
|
||||||
|
|
|
@ -55,7 +55,10 @@ class PlayState(object):
|
||||||
|
|
||||||
# Set by SpecialMonitor - did user choose to resume playback or start from the
|
# Set by SpecialMonitor - did user choose to resume playback or start from the
|
||||||
# beginning?
|
# beginning?
|
||||||
self.resume_playback = False
|
# 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?
|
||||||
|
|
|
@ -60,11 +60,6 @@ class ContextMenu(object):
|
||||||
self.api = API(xml[0])
|
self.api = API(xml[0])
|
||||||
if self._select_menu():
|
if self._select_menu():
|
||||||
self._action_menu()
|
self._action_menu()
|
||||||
if self._selected_option in (OPTIONS['Delete'],
|
|
||||||
OPTIONS['Refresh']):
|
|
||||||
LOG.info("refreshing container")
|
|
||||||
app.APP.monitor.waitForAbort(0.5)
|
|
||||||
xbmc.executebuiltin('Container.Refresh')
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_plex_id(kodi_id, kodi_type):
|
def _get_plex_id(kodi_id, kodi_type):
|
||||||
|
@ -147,7 +142,7 @@ class ContextMenu(object):
|
||||||
v.KODI_PLAYLIST_TYPE_FROM_KODI_TYPE[self.kodi_type])
|
v.KODI_PLAYLIST_TYPE_FROM_KODI_TYPE[self.kodi_type])
|
||||||
playqueue.clear()
|
playqueue.clear()
|
||||||
app.PLAYSTATE.context_menu_play = True
|
app.PLAYSTATE.context_menu_play = True
|
||||||
handle = self.api.path(force_first_media=False, force_addon=True)
|
handle = self.api.path(force_addon=True)
|
||||||
handle = 'RunPlugin(%s)' % handle
|
handle = 'RunPlugin(%s)' % handle
|
||||||
xbmc.executebuiltin(handle.encode('utf-8'))
|
xbmc.executebuiltin(handle.encode('utf-8'))
|
||||||
|
|
||||||
|
|
|
@ -170,10 +170,10 @@ def stop():
|
||||||
|
|
||||||
def seek_to(offset):
|
def seek_to(offset):
|
||||||
"""
|
"""
|
||||||
Seeks all Kodi players to offset [int]
|
Seeks all Kodi players to offset [int] in milliseconds
|
||||||
"""
|
"""
|
||||||
for playerid in get_player_ids():
|
for playerid in get_player_ids():
|
||||||
JsonRPC("Player.Seek").execute(
|
return JsonRPC("Player.Seek").execute(
|
||||||
{"playerid": playerid,
|
{"playerid": playerid,
|
||||||
"value": timing.millis_to_kodi_time(offset)})
|
"value": timing.millis_to_kodi_time(offset)})
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
from __future__ import absolute_import, division, unicode_literals
|
from __future__ import absolute_import, division, unicode_literals
|
||||||
import xbmc
|
import xbmc
|
||||||
|
|
||||||
from .. import utils, variables as v
|
from .. import utils, app, variables as v
|
||||||
|
|
||||||
PLAYLIST_SYNC_ENABLED = (v.DEVICE != 'Microsoft UWP' and
|
PLAYLIST_SYNC_ENABLED = (v.DEVICE != 'Microsoft UWP' and
|
||||||
utils.settings('enablePlaylistSync') == 'true')
|
utils.settings('enablePlaylistSync') == 'true')
|
||||||
|
@ -42,7 +42,12 @@ def update_kodi_library(video=True, music=True):
|
||||||
Updates the Kodi library and thus refreshes the Kodi views and widgets
|
Updates the Kodi library and thus refreshes the Kodi views and widgets
|
||||||
"""
|
"""
|
||||||
if video:
|
if video:
|
||||||
|
if not xbmc.getCondVisibility('Window.IsMedia'):
|
||||||
xbmc.executebuiltin('UpdateLibrary(video)')
|
xbmc.executebuiltin('UpdateLibrary(video)')
|
||||||
|
else:
|
||||||
|
# Prevent cursor from moving - refresh later
|
||||||
|
xbmc.executebuiltin('Container.Refresh')
|
||||||
|
app.APP.update_widgets = True
|
||||||
if music:
|
if music:
|
||||||
xbmc.executebuiltin('UpdateLibrary(music)')
|
xbmc.executebuiltin('UpdateLibrary(music)')
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,9 @@ Used to kick off Kodi playback
|
||||||
from __future__ import absolute_import, division, unicode_literals
|
from __future__ import absolute_import, division, unicode_literals
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
import xbmc
|
||||||
|
|
||||||
from .plex_api import API
|
from .plex_api import API
|
||||||
from .plex_db import PlexDB
|
from .plex_db import PlexDB
|
||||||
|
@ -43,6 +46,17 @@ def playback_triage(plex_id=None, plex_type=None, path=None, resolve=True):
|
||||||
the first pass - e.g. if you're calling this function from the original
|
the first pass - e.g. if you're calling this function from the original
|
||||||
service.py Python instance
|
service.py Python instance
|
||||||
"""
|
"""
|
||||||
|
try:
|
||||||
|
_playback_triage(plex_id, plex_type, path, resolve)
|
||||||
|
finally:
|
||||||
|
# Reset some playback variables the user potentially set to init
|
||||||
|
# playback
|
||||||
|
app.PLAYSTATE.context_menu_play = False
|
||||||
|
app.PLAYSTATE.force_transcode = False
|
||||||
|
app.PLAYSTATE.resume_playback = None
|
||||||
|
|
||||||
|
|
||||||
|
def _playback_triage(plex_id, plex_type, path, resolve):
|
||||||
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.info('playback_triage called with plex_id %s, plex_type %s, path %s, '
|
||||||
'resolve %s', plex_id, plex_type, path, resolve)
|
'resolve %s', plex_id, plex_type, path, resolve)
|
||||||
|
@ -118,6 +132,9 @@ def playback_triage(plex_id=None, plex_type=None, path=None, resolve=True):
|
||||||
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)
|
||||||
else:
|
else:
|
||||||
|
@ -163,22 +180,26 @@ def _playback_init(plex_id, plex_type, playqueue, pos):
|
||||||
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.info('Initializing PKC playback')
|
||||||
|
# Stop playback so we don't get an error message that the last item of the
|
||||||
|
# queue failed to play
|
||||||
|
app.APP.player.stop()
|
||||||
xml = PF.GetPlexMetadata(plex_id, reraise=True)
|
xml = PF.GetPlexMetadata(plex_id, reraise=True)
|
||||||
if xml in (None, 401):
|
if xml in (None, 401):
|
||||||
LOG.error('Could not get a PMS xml for plex id %s', plex_id)
|
LOG.error('Could not get a PMS xml for plex id %s', plex_id)
|
||||||
_ensure_resolve(abort=True)
|
_ensure_resolve(abort=True)
|
||||||
return
|
return
|
||||||
if playqueue.kodi_pl.size() > 1:
|
if (xbmc.getCondVisibility('Window.IsVisible(Home.xml)') and
|
||||||
|
plex_type in v.PLEX_VIDEOTYPES and
|
||||||
|
playqueue.kodi_pl.size() > 1):
|
||||||
|
# playqueue.kodi_pl.size() could return more than one - since playback
|
||||||
|
# was initiated from the audio queue!
|
||||||
|
LOG.debug('Detected widget playback for videos')
|
||||||
|
elif playqueue.kodi_pl.size() > 1:
|
||||||
# Special case - we already got a filled Kodi playqueue
|
# Special case - we already got a filled Kodi playqueue
|
||||||
try:
|
try:
|
||||||
_init_existing_kodi_playlist(playqueue, pos)
|
_init_existing_kodi_playlist(playqueue, pos)
|
||||||
except PL.PlaylistError:
|
except PL.PlaylistError:
|
||||||
LOG.error('Playback_init for existing Kodi playlist failed')
|
LOG.error('Playback_init for existing Kodi playlist failed')
|
||||||
# "Play error"
|
|
||||||
utils.dialog('notification',
|
|
||||||
utils.lang(29999),
|
|
||||||
utils.lang(30128),
|
|
||||||
icon='{error}')
|
|
||||||
_ensure_resolve(abort=True)
|
_ensure_resolve(abort=True)
|
||||||
return
|
return
|
||||||
# Now we need to use setResolvedUrl for the item at position ZERO
|
# Now we need to use setResolvedUrl for the item at position ZERO
|
||||||
|
@ -187,19 +208,29 @@ def _playback_init(plex_id, plex_type, playqueue, pos):
|
||||||
return
|
return
|
||||||
# "Usual" case - consider trailers and parts and build both Kodi and Plex
|
# "Usual" case - consider trailers and parts and build both Kodi and Plex
|
||||||
# playqueues
|
# playqueues
|
||||||
# Pass dummy PKC video with 0 length so Kodi immediately stops playback
|
# Release default.py
|
||||||
# and we can build our own playqueue.
|
|
||||||
_ensure_resolve()
|
_ensure_resolve()
|
||||||
api = API(xml[0])
|
api = API(xml[0])
|
||||||
|
if app.SYNC.direct_paths and api.resume_point():
|
||||||
|
# Since Kodi won't ask if user wants to resume playback -
|
||||||
|
# we need to ask ourselves
|
||||||
|
resume = resume_dialog(int(api.resume_point()))
|
||||||
|
if resume is None:
|
||||||
|
LOG.info('User cancelled resume dialog')
|
||||||
|
return
|
||||||
|
elif app.SYNC.direct_paths:
|
||||||
|
resume = False
|
||||||
|
else:
|
||||||
|
resume = app.PLAYSTATE.resume_playback or False
|
||||||
trailers = False
|
trailers = False
|
||||||
if (plex_type == v.PLEX_TYPE_MOVIE and not api.resume_point() and
|
if (not resume and plex_type == v.PLEX_TYPE_MOVIE and
|
||||||
utils.settings('enableCinema') == "true"):
|
utils.settings('enableCinema') == "true"):
|
||||||
if utils.settings('askCinema') == "true":
|
if utils.settings('askCinema') == "true":
|
||||||
# "Play trailers?"
|
# "Play trailers?"
|
||||||
trailers = utils.yesno_dialog(utils.lang(29999), utils.lang(33016))
|
trailers = utils.yesno_dialog(utils.lang(29999), utils.lang(33016))
|
||||||
else:
|
else:
|
||||||
trailers = True
|
trailers = True
|
||||||
LOG.debug('Playing trailers: %s', trailers)
|
LOG.debug('Resuming: %s. Playing trailers: %s', resume, trailers)
|
||||||
playqueue.clear()
|
playqueue.clear()
|
||||||
if plex_type != v.PLEX_TYPE_CLIP:
|
if plex_type != v.PLEX_TYPE_CLIP:
|
||||||
# Post to the PMS to create a playqueue - in any case due to Companion
|
# Post to the PMS to create a playqueue - in any case due to Companion
|
||||||
|
@ -216,25 +247,15 @@ def _playback_init(plex_id, plex_type, playqueue, pos):
|
||||||
utils.lang(30128),
|
utils.lang(30128),
|
||||||
icon='{error}')
|
icon='{error}')
|
||||||
# Do NOT use _ensure_resolve() because we resolved above already
|
# Do NOT use _ensure_resolve() because we resolved above already
|
||||||
app.PLAYSTATE.context_menu_play = False
|
|
||||||
app.PLAYSTATE.force_transcode = False
|
|
||||||
app.PLAYSTATE.resume_playback = False
|
|
||||||
return
|
return
|
||||||
PL.get_playlist_details_from_xml(playqueue, xml)
|
PL.get_playlist_details_from_xml(playqueue, xml)
|
||||||
stack = _prep_playlist_stack(xml)
|
stack = _prep_playlist_stack(xml, resume)
|
||||||
_process_stack(playqueue, stack)
|
_process_stack(playqueue, stack)
|
||||||
# Always resume if playback initiated via PMS and there IS a resume
|
|
||||||
# point
|
|
||||||
offset = api.resume_point() * 1000 if app.PLAYSTATE.context_menu_play else None
|
|
||||||
# Reset some playback variables
|
|
||||||
app.PLAYSTATE.context_menu_play = False
|
|
||||||
app.PLAYSTATE.force_transcode = False
|
|
||||||
# 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, offset))
|
args=(playqueue.kodi_pl, pos, None))
|
||||||
thread.setDaemon(True)
|
thread.setDaemon(True)
|
||||||
LOG.info('Done initializing playback, starting Kodi player at pos %s and '
|
LOG.info('Done initializing playback, starting Kodi player at pos %s', pos)
|
||||||
'resume point %s', pos, offset)
|
|
||||||
# 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
|
||||||
|
@ -258,13 +279,30 @@ def _ensure_resolve(abort=False):
|
||||||
if RESOLVE:
|
if RESOLVE:
|
||||||
# Releases the other Python thread without a ListItem
|
# Releases the other Python thread without a ListItem
|
||||||
transfer.send(True)
|
transfer.send(True)
|
||||||
# Shows PKC error message
|
# Wait for default.py to have completed xbmcplugin.setResolvedUrl()
|
||||||
# transfer.send(None)
|
transfer.wait_for_transfer(source='default')
|
||||||
if abort:
|
if abort:
|
||||||
# Reset some playback variables
|
utils.dialog('notification',
|
||||||
app.PLAYSTATE.context_menu_play = False
|
heading='{plex}',
|
||||||
app.PLAYSTATE.force_transcode = False
|
message=utils.lang(30128),
|
||||||
app.PLAYSTATE.resume_playback = False
|
icon='{error}',
|
||||||
|
time=3000)
|
||||||
|
|
||||||
|
|
||||||
|
def resume_dialog(resume):
|
||||||
|
"""
|
||||||
|
Pass the resume [int] point in seconds. Returns True if user chose to
|
||||||
|
resume. Returns None if user cancelled
|
||||||
|
"""
|
||||||
|
# "Resume from {0:s}"
|
||||||
|
# "Start from beginning"
|
||||||
|
resume = datetime.timedelta(seconds=resume)
|
||||||
|
answ = utils.dialog('contextmenu',
|
||||||
|
[utils.lang(12022).replace('{0:s}', '{0}').format(unicode(resume)),
|
||||||
|
utils.lang(12021)])
|
||||||
|
if answ == -1:
|
||||||
|
return
|
||||||
|
return answ == 0
|
||||||
|
|
||||||
|
|
||||||
def _init_existing_kodi_playlist(playqueue, pos):
|
def _init_existing_kodi_playlist(playqueue, pos):
|
||||||
|
@ -285,9 +323,13 @@ def _init_existing_kodi_playlist(playqueue, pos):
|
||||||
LOG.debug('Done init_existing_kodi_playlist')
|
LOG.debug('Done init_existing_kodi_playlist')
|
||||||
|
|
||||||
|
|
||||||
def _prep_playlist_stack(xml):
|
def _prep_playlist_stack(xml, resume):
|
||||||
|
"""
|
||||||
|
resume [bool] will set the resume point of the LAST item of the stack, for
|
||||||
|
part 1 only
|
||||||
|
"""
|
||||||
stack = []
|
stack = []
|
||||||
for item in xml:
|
for i, item in enumerate(xml):
|
||||||
api = API(item)
|
api = API(item)
|
||||||
if (app.PLAYSTATE.context_menu_play is False and
|
if (app.PLAYSTATE.context_menu_play is False and
|
||||||
api.plex_type() not in (v.PLEX_TYPE_CLIP, v.PLEX_TYPE_EPISODE)):
|
api.plex_type() not in (v.PLEX_TYPE_CLIP, v.PLEX_TYPE_EPISODE)):
|
||||||
|
@ -310,9 +352,17 @@ def _prep_playlist_stack(xml):
|
||||||
api.set_part_number(part)
|
api.set_part_number(part)
|
||||||
if kodi_id is None:
|
if kodi_id is None:
|
||||||
# Need to redirect again to PKC to conclude playback
|
# Need to redirect again to PKC to conclude playback
|
||||||
path = api.path()
|
path = api.path(force_addon=True, force_first_media=True)
|
||||||
|
# Using different paths than the ones saved in the Kodi DB
|
||||||
|
# fixes Kodi immediately resuming the video if one restarts
|
||||||
|
# the same video again after playback
|
||||||
|
# WARNING: This fixes startup, but renders Kodi unstable
|
||||||
|
# path = path.replace('plugin.video.plexkodiconnect.tvshows',
|
||||||
|
# 'plugin.video.plexkodiconnect', 1)
|
||||||
|
# path = path.replace('plugin.video.plexkodiconnect.movies',
|
||||||
|
# 'plugin.video.plexkodiconnect', 1)
|
||||||
listitem = api.create_listitem()
|
listitem = api.create_listitem()
|
||||||
listitem.setPath(utils.try_encode(path))
|
listitem.setPath(path.encode('utf-8'))
|
||||||
else:
|
else:
|
||||||
# Will add directly via the Kodi DB
|
# Will add directly via the Kodi DB
|
||||||
path = None
|
path = None
|
||||||
|
@ -326,6 +376,7 @@ def _prep_playlist_stack(xml):
|
||||||
'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,
|
||||||
'id': api.item_id()
|
'id': api.item_id()
|
||||||
})
|
})
|
||||||
return stack
|
return stack
|
||||||
|
@ -358,9 +409,39 @@ def _process_stack(playqueue, stack):
|
||||||
playlist_item.part = item['part']
|
playlist_item.part = item['part']
|
||||||
playlist_item.id = item['id']
|
playlist_item.id = item['id']
|
||||||
playlist_item.force_transcode = app.PLAYSTATE.force_transcode
|
playlist_item.force_transcode = app.PLAYSTATE.force_transcode
|
||||||
|
playlist_item.resume = item['resume']
|
||||||
pos += 1
|
pos += 1
|
||||||
|
|
||||||
|
|
||||||
|
def _set_resume(listitem, item, api):
|
||||||
|
if item.plex_type in (v.PLEX_TYPE_SONG, v.PLEX_TYPE_CLIP):
|
||||||
|
return
|
||||||
|
if item.resume is True:
|
||||||
|
# Do NOT use item.offset directly but get it from the DB
|
||||||
|
# (user might have initiated same video twice)
|
||||||
|
with PlexDB(lock=False) as plexdb:
|
||||||
|
db_item = plexdb.item_by_id(item.plex_id, item.plex_type)
|
||||||
|
if db_item:
|
||||||
|
file_id = db_item['kodi_fileid']
|
||||||
|
with KodiVideoDB(lock=False) as kodidb:
|
||||||
|
item.offset = kodidb.get_resume(file_id)
|
||||||
|
LOG.info('Resuming playback at %s', item.offset)
|
||||||
|
if v.KODIVERSION >= 18 and api:
|
||||||
|
# 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):
|
||||||
"""
|
"""
|
||||||
ONLY if actually being played (e.g. at 5th position of a playqueue).
|
ONLY if actually being played (e.g. at 5th position of a playqueue).
|
||||||
|
@ -391,40 +472,14 @@ def _conclude_playback(playqueue, pos):
|
||||||
playurl = item.file
|
playurl = item.file
|
||||||
if not playurl:
|
if not playurl:
|
||||||
LOG.info('Did not get a playurl, aborting playback silently')
|
LOG.info('Did not get a playurl, aborting playback silently')
|
||||||
app.PLAYSTATE.resume_playback = False
|
_ensure_resolve(abort=True)
|
||||||
transfer.send(True)
|
|
||||||
return
|
return
|
||||||
listitem.setPath(utils.try_encode(playurl))
|
listitem.setPath(playurl.encode('utf-8'))
|
||||||
if item.playmethod == 'DirectStream':
|
if item.playmethod == 'DirectStream':
|
||||||
listitem.setSubtitles(api.cache_external_subs())
|
listitem.setSubtitles(api.cache_external_subs())
|
||||||
elif item.playmethod == 'Transcode':
|
elif item.playmethod == 'Transcode':
|
||||||
playutils.audio_subtitle_prefs(listitem)
|
playutils.audio_subtitle_prefs(listitem)
|
||||||
|
_set_resume(listitem, item, api)
|
||||||
if app.PLAYSTATE.resume_playback is True:
|
|
||||||
app.PLAYSTATE.resume_playback = False
|
|
||||||
if item.plex_type not in (v.PLEX_TYPE_SONG, v.PLEX_TYPE_CLIP):
|
|
||||||
# Do NOT use item.offset directly but get it from the DB
|
|
||||||
# (user might have initiated same video twice)
|
|
||||||
with PlexDB(lock=False) as plexdb:
|
|
||||||
db_item = plexdb.item_by_id(item.plex_id, item.plex_type)
|
|
||||||
file_id = db_item['kodi_fileid'] if db_item else None
|
|
||||||
with KodiVideoDB(lock=False) as kodidb:
|
|
||||||
item.offset = kodidb.get_resume(file_id)
|
|
||||||
LOG.info('Resuming playback at %s', item.offset)
|
|
||||||
if v.KODIVERSION >= 18 and api:
|
|
||||||
# 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:
|
|
||||||
listitem.setProperty('StartPercent', '0')
|
|
||||||
# Reset the resumable flag
|
|
||||||
transfer.send(listitem)
|
transfer.send(listitem)
|
||||||
LOG.info('Done concluding playback')
|
LOG.info('Done concluding playback')
|
||||||
|
|
||||||
|
@ -512,23 +567,22 @@ 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
|
||||||
LOG.info("play_xml called with offset %s, start_plex_id %s",
|
LOG.info("play_xml called with offset %s, start_plex_id %s",
|
||||||
offset, start_plex_id)
|
offset, start_plex_id)
|
||||||
stack = _prep_playlist_stack(xml)
|
start_item = start_plex_id if start_plex_id is not None \
|
||||||
|
else playqueue.selectedItemID
|
||||||
|
for startpos, video in enumerate(xml):
|
||||||
|
api = API(video)
|
||||||
|
if api.plex_id() == start_item:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
startpos = 0
|
||||||
|
stack = _prep_playlist_stack(xml, resume=False)
|
||||||
|
if offset:
|
||||||
|
stack[startpos]['resume'] = True
|
||||||
_process_stack(playqueue, stack)
|
_process_stack(playqueue, stack)
|
||||||
LOG.debug('Playqueue after play_xml update: %s', playqueue)
|
LOG.debug('Playqueue after play_xml update: %s', playqueue)
|
||||||
if start_plex_id is not None:
|
|
||||||
for startpos, item in enumerate(playqueue.items):
|
|
||||||
if item.plex_id == start_plex_id:
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
startpos = 0
|
|
||||||
else:
|
|
||||||
for startpos, item in enumerate(playqueue.items):
|
|
||||||
if item.id == playqueue.selectedItemID:
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
startpos = 0
|
|
||||||
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.info('Done play_xml, starting Kodi player at position %s', startpos)
|
||||||
|
@ -542,7 +596,7 @@ def threaded_playback(kodi_playlist, 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':
|
if offset and offset != '0':
|
||||||
i = 0
|
i = 0
|
||||||
while not app.APP.is_playing:
|
while not app.APP.is_playing or not js.get_player_ids():
|
||||||
app.APP.monitor.waitForAbort(0.1)
|
app.APP.monitor.waitForAbort(0.1)
|
||||||
i += 1
|
i += 1
|
||||||
if i > 100:
|
if i > 100:
|
||||||
|
|
|
@ -29,6 +29,8 @@ class PlaybackTask(backgroundthread.Task):
|
||||||
# E.g. other add-ons scanning for Extras folder
|
# E.g. other add-ons scanning for Extras folder
|
||||||
LOG.debug('Detected 3rd party add-on call - ignoring')
|
LOG.debug('Detected 3rd party add-on call - ignoring')
|
||||||
transfer.send(True)
|
transfer.send(True)
|
||||||
|
# Wait for default.py to have completed xbmcplugin.setResolvedUrl()
|
||||||
|
transfer.wait_for_transfer(source='default')
|
||||||
return
|
return
|
||||||
params = dict(utils.parse_qsl(params))
|
params = dict(utils.parse_qsl(params))
|
||||||
mode = params.get('mode')
|
mode = params.get('mode')
|
||||||
|
|
|
@ -167,6 +167,11 @@ class Playlist_Item(object):
|
||||||
# If Plex video consists of several parts; part number
|
# If Plex video consists of several parts; part number
|
||||||
self._part = 0
|
self._part = 0
|
||||||
self.force_transcode = False
|
self.force_transcode = False
|
||||||
|
# Shall we ask user to resume this item?
|
||||||
|
# None: ask user to resume
|
||||||
|
# False: do NOT resume, don't ask user
|
||||||
|
# True: do resume, don't ask user
|
||||||
|
self.resume = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def plex_id(self):
|
def plex_id(self):
|
||||||
|
|
|
@ -1388,6 +1388,7 @@ class API(object):
|
||||||
option = cast(str, option.strip())
|
option = cast(str, option.strip())
|
||||||
dialoglist.append(option)
|
dialoglist.append(option)
|
||||||
media = utils.dialog('select', 'Select stream', dialoglist)
|
media = utils.dialog('select', 'Select stream', dialoglist)
|
||||||
|
LOG.info('User chose media stream number: %s', media)
|
||||||
if media == -1:
|
if media == -1:
|
||||||
LOG.info('User cancelled media stream selection')
|
LOG.info('User cancelled media stream selection')
|
||||||
return
|
return
|
||||||
|
|
|
@ -486,6 +486,8 @@ class Service(object):
|
||||||
elif plex_command == 'EXIT-PKC':
|
elif plex_command == 'EXIT-PKC':
|
||||||
LOG.info('Received command from another instance to quit')
|
LOG.info('Received command from another instance to quit')
|
||||||
app.APP.stop_pkc = True
|
app.APP.stop_pkc = True
|
||||||
|
else:
|
||||||
|
raise RuntimeError('Unknown command: %s', plex_command)
|
||||||
if task:
|
if task:
|
||||||
backgroundthread.BGThreader.addTasksToFront([task])
|
backgroundthread.BGThreader.addTasksToFront([task])
|
||||||
continue
|
continue
|
||||||
|
@ -494,6 +496,15 @@ class Service(object):
|
||||||
xbmc.sleep(100)
|
xbmc.sleep(100)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
if app.APP.update_widgets and not xbmc.getCondVisibility('Window.IsMedia'):
|
||||||
|
'''
|
||||||
|
In case an update happened but we were not on the homescreen
|
||||||
|
and now we are, force widgets to update. Prevents cursor from
|
||||||
|
moving/jumping in libraries
|
||||||
|
'''
|
||||||
|
app.APP.update_widgets = False
|
||||||
|
xbmc.executebuiltin('UpdateLibrary(video)')
|
||||||
|
|
||||||
# Before proceeding, need to make sure:
|
# Before proceeding, need to make sure:
|
||||||
# 1. Server is online
|
# 1. Server is online
|
||||||
# 2. User is set
|
# 2. User is set
|
||||||
|
|
|
@ -14,7 +14,8 @@ import xbmcgui
|
||||||
LOG = getLogger('PLEX.transfer')
|
LOG = getLogger('PLEX.transfer')
|
||||||
MONITOR = xbmc.Monitor()
|
MONITOR = xbmc.Monitor()
|
||||||
WINDOW = xbmcgui.Window(10000)
|
WINDOW = xbmcgui.Window(10000)
|
||||||
WINDOW_RESULT = 'plexkodiconnect.result'.encode('utf-8')
|
WINDOW_UPSTREAM = 'plexkodiconnect.result.upstream'.encode('utf-8')
|
||||||
|
WINDOW_DOWNSTREAM = 'plexkodiconnect.result.downstream'.encode('utf-8')
|
||||||
WINDOW_COMMAND = 'plexkodiconnect.command'.encode('utf-8')
|
WINDOW_COMMAND = 'plexkodiconnect.command'.encode('utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
@ -90,7 +91,7 @@ def de_serialize(answ):
|
||||||
raise NotImplementedError('Not implemented: %s' % answ)
|
raise NotImplementedError('Not implemented: %s' % answ)
|
||||||
|
|
||||||
|
|
||||||
def send(pkc_listitem):
|
def send(pkc_listitem, target='default'):
|
||||||
"""
|
"""
|
||||||
Pickles the obj to the window variable. Use to transfer Python
|
Pickles the obj to the window variable. Use to transfer Python
|
||||||
objects between different PKC python instances (e.g. if default.py is
|
objects between different PKC python instances (e.g. if default.py is
|
||||||
|
@ -98,18 +99,27 @@ def send(pkc_listitem):
|
||||||
|
|
||||||
obj can be pretty much any Python object. However, classes and
|
obj can be pretty much any Python object. However, classes and
|
||||||
functions won't work. See the Pickle documentation
|
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)
|
LOG.debug('Sending: %s', pkc_listitem)
|
||||||
kodi_window(WINDOW_RESULT,
|
kodi_window(window,
|
||||||
value=json.dumps(serialize(pkc_listitem)))
|
value=json.dumps(serialize(pkc_listitem)))
|
||||||
|
|
||||||
|
|
||||||
def wait_for_transfer():
|
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
|
||||||
|
"""
|
||||||
|
window = WINDOW_DOWNSTREAM if source == 'main' else WINDOW_UPSTREAM
|
||||||
result = ''
|
result = ''
|
||||||
while not result:
|
while not result:
|
||||||
result = kodi_window(WINDOW_RESULT)
|
result = kodi_window(window)
|
||||||
if result:
|
if result:
|
||||||
kodi_window(WINDOW_RESULT, clear=True)
|
kodi_window(window, clear=True)
|
||||||
LOG.debug('Received')
|
LOG.debug('Received')
|
||||||
result = json.loads(result)
|
result = json.loads(result)
|
||||||
return de_serialize(result)
|
return de_serialize(result)
|
||||||
|
|
|
@ -211,7 +211,8 @@ def dialog(typus, *args, **kwargs):
|
||||||
'notification': dia.notification,
|
'notification': dia.notification,
|
||||||
'input': dia.input,
|
'input': dia.input,
|
||||||
'select': dia.select,
|
'select': dia.select,
|
||||||
'numeric': dia.numeric
|
'numeric': dia.numeric,
|
||||||
|
'contextmenu': dia.contextmenu
|
||||||
}
|
}
|
||||||
return types[typus](*args, **kwargs)
|
return types[typus](*args, **kwargs)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue