Merge pull request #1038 from croneter/beta-version

Bump master
This commit is contained in:
croneter 2019-11-02 12:14:35 +01:00 committed by GitHub
commit 6376fecbf7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 236 additions and 280 deletions

View file

@ -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)
[![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)
[![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.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)
[![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
- Kodi 19 Matrix is not yet supported (PKC is written in Python 2)
- Support for Kodi 18 Leia
- Support for Kodi 17 Krypton
- [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/)
- [Plex Watch Later / Plex It!](https://support.plex.tv/hc/en-us/sections/200211783-Plex-It-)

View file

@ -1,11 +1,11 @@
<?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>
<import addon="xbmc.python" version="2.1.0"/>
<import addon="script.module.requests" version="2.9.1" />
<import addon="script.module.defusedxml" version="0.5.0"/>
<import addon="plugin.video.plexkodiconnect.movies" version="2.1.1" />
<import addon="plugin.video.plexkodiconnect.tvshows" version="2.1.1" />
<import addon="plugin.video.plexkodiconnect.movies" version="2.1.2" />
<import addon="plugin.video.plexkodiconnect.tvshows" version="2.1.2" />
</requires>
<extension point="xbmc.python.pluginsource" library="default.py">
<provides>video audio image</provides>
@ -83,7 +83,27 @@
<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>
<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 (beta only):

View file

@ -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.10 for everyone

View file

@ -61,6 +61,13 @@ class Main():
elif mode == 'channels':
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':
# Hack so we can store this path in the Kodi DB
handle = ('plugin://%s?mode=extras&plex_id=%s'

View file

@ -56,12 +56,6 @@ class PlayState(object):
# Currently playing PKC item, a PlaylistItem()
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?
self.context_menu_play = False
# Set by context menu - shall we force-transcode the next playing item?

View file

@ -3,6 +3,8 @@
from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
import xbmc
from . import utils
from . import variables as v
@ -31,7 +33,7 @@ def getXArgsDeviceInfo(options=None, include_token=True):
'Connection': 'keep-alive',
"Content-Type": "application/x-www-form-urlencoded",
# "Access-Control-Allow-Origin": "*",
# 'X-Plex-Language': 'en',
'Accept-Language': xbmc.getLanguage(xbmc.ISO_639_1),
'X-Plex-Device': v.DEVICE,
'X-Plex-Model': v.MODEL,
'X-Plex-Device-Name': v.DEVICENAME,

View file

@ -146,6 +146,8 @@ def show_main_menu(content_type=None):
if content_type:
path += '&content_type=%s' % content_type
directory_item('Plex Hub', path)
# Plex Search "Search"
directory_item(utils.lang(137), "plugin://%s?mode=search" % v.ADDON_ID)
# Plex Watch later
if content_type not in ('image', 'audio'):
directory_item(utils.lang(39211),
@ -466,7 +468,7 @@ def watchlater():
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
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
"""
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():
xbmcplugin.endOfDirectory(int(sys.argv[1]), False)
return
app.init(entrypoint=True)
args = args or {}
if prompt:
prompt = utils.dialog('input', prompt)
if prompt is None:
# User cancelled
return
prompt = prompt.strip().decode('utf-8')
if '?' not in key:
key = '%s?query=%s' % (key, prompt)
else:
key = '%s&query=%s' % (key, prompt)
xml = DU().downloadUrl('{server}%s' % key)
args['query'] = prompt
xml = DU().downloadUrl(utils.extend_url('{server}%s' % key, args))
try:
xml.attrib
except AttributeError:
xml[0].attrib
except (TypeError, IndexError, AttributeError):
LOG.error('Could not browse to key %s, section %s',
key, section_id)
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)

View file

@ -5,7 +5,7 @@ from logging import getLogger
from sqlite3 import IntegrityError
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')

View file

@ -11,21 +11,17 @@ import json
import binascii
import xbmc
import xbmcgui
from .plex_api import API
from .plex_db import PlexDB
from . import kodi_db
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 backgroundthread, app, variables as v
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):
"""
@ -33,7 +29,6 @@ class KodiMonitor(xbmc.Monitor):
"""
def __init__(self):
self._already_slept = False
self.hack_replay = None
xbmc.Monitor.__init__(self)
for playerid in app.PLAYSTATE.player_states:
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
"""
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):
"""
@ -69,28 +61,12 @@ class KodiMonitor(xbmc.Monitor):
data = loads(data, 'utf-8')
LOG.debug("Method: %s Data: %s", method, data)
# Hack
if not method == 'Player.OnStop':
self.hack_replay = None
if method == "Player.OnPlay":
with app.APP.lock_playqueues:
self.PlayBackStart(data)
elif method == "Player.OnStop":
# Should refresh our video nodes, e.g. on deck
# xbmc.executebuiltin('ReloadSkin()')
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()
with app.APP.lock_playqueues:
_playback_cleanup(ended=data.get('end'))
elif method == 'Playlist.OnAdd':
if 'item' in data and data['item'].get('type') == v.KODI_TYPE_SHOW:
# 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':
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):
"""
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
"""
if 'id' not in data['item']:
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']
pass
def _playlist_onremove(self, data):
"""
@ -451,7 +386,7 @@ def _playback_cleanup(ended=False):
app.PLAYSTATE.active_players = set()
app.PLAYSTATE.item = None
utils.delete_temporary_subtitles()
LOG.info('Finished PKC playback cleanup')
LOG.debug('Finished PKC playback cleanup')
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.
"""
LOG.debug('Start cleaning Kodi files table')
app.APP.monitor.waitForAbort(2)
if app.APP.monitor.waitForAbort(2):
# PKC should exit
return
try:
with kodi_db.KodiVideoDB() as kodidb_1:
with kodi_db.KodiVideoDB(lock=False) as kodidb_2:
for file_id in kodidb_1.obsolete_file_ids():
LOG.debug('Removing obsolete Kodi file_id %s', file_id)
kodidb_2.remove_file(file_id, remove_orphans=False)
with kodi_db.KodiVideoDB() as kodidb:
obsolete_file_ids = list(kodidb.obsolete_file_ids())
for file_id in obsolete_file_ids:
LOG.debug('Removing obsolete Kodi file_id %s', file_id)
kodidb.remove_file(file_id, remove_orphans=False)
except utils.OperationalError:
LOG.debug('Database was locked, unable to clean file table')
else:
@ -653,36 +590,3 @@ def _videolibrary_onupdate(data):
PF.scrobble(db_item['plex_id'], 'watched')
else:
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)

View file

@ -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.
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
"""
try:
_playback_triage(plex_id, plex_type, path, resolve)
_playback_triage(plex_id, plex_type, path, resolve, resume)
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):
def _playback_triage(plex_id, plex_type, path, resolve, resume):
plex_id = utils.cast(int, plex_id)
LOG.info('playback_triage called with plex_id %s, plex_type %s, path %s, '
'resolve %s', plex_id, plex_type, path, resolve)
LOG.debug('playback_triage called with plex_id %s, plex_type %s, path %s, '
'resolve %s, resume %s', plex_id, plex_type, path, resolve, resume)
global RESOLVE
# If started via Kodi context menu, we never resolve
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:
# Kodi bug - Playlist plays (not Playqueue) will ALWAYS be audio for
# 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)
try:
pos = js.get_position(playqueue.playlistid)
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)
try:
pos = js.get_position(playqueue.playlistid)
@ -108,12 +108,12 @@ def _playback_triage(plex_id, plex_type, path, resolve):
try:
item = items[pos]
except IndexError:
LOG.info('Could not apply playlist hack! Probably Widget playback')
LOG.debug('Could not apply playlist hack! Probably Widget playback')
else:
if ('id' not in item and
item.get('type') == 'unknown' and item.get('title') == ''):
LOG.info('Kodi playlist play detected')
_playlist_playback(plex_id, plex_type)
LOG.debug('Kodi playlist play detected')
_playlist_playback(plex_id)
return
# Can return -1 (as in "no playlist")
@ -127,22 +127,20 @@ def _playback_triage(plex_id, plex_type, path, resolve):
initiate = True
else:
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)
initiate = True
else:
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:
_playback_init(plex_id, plex_type, playqueue, pos)
_playback_init(plex_id, plex_type, playqueue, pos, resume)
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)
def _playlist_playback(plex_id, plex_type):
def _playlist_playback(plex_id):
"""
Really annoying Kodi behavior: Kodi will throw the ENTIRE playlist some-
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)
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.
"""
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
# queue failed to play
app.APP.player.stop()
@ -211,18 +209,17 @@ def _playback_init(plex_id, plex_type, playqueue, pos):
# Release default.py
_ensure_resolve()
api = API(xml[0])
if api.resume_point() and (app.SYNC.direct_paths or
app.PLAYSTATE.context_menu_play):
# Since Kodi won't ask if user wants to resume playback -
# we need to ask ourselves
if (app.PLAYSTATE.context_menu_play and
api.resume_point() and
api.plex_type in v.PLEX_VIDEOTYPES):
# 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()))
if resume is None:
LOG.info('User cancelled resume dialog')
# User cancelled dialog
return
elif app.SYNC.direct_paths:
resume = False
else:
resume = app.PLAYSTATE.resume_playback or False
LOG.debug('Using resume %s', resume)
resume = resume or False
trailers = False
if (not resume and plex_type == v.PLEX_TYPE_MOVIE and
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)
stack = _prep_playlist_stack(xml, resume)
_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)
thread = Thread(target=threaded_playback,
args=(playqueue.kodi_pl, pos, None))
args=(playqueue.kodi_pl, pos, offset))
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
# caches paths like our plugin://pkc. If we use Player().play() between
# 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
# path: log entry is "CGUIWindowVideoBase::OnPlayMedia <missing path>"
thread.start()
# Ensure that PKC playqueue monitor ignores the changes we just made
playqueue.pkc_edit = True
def _ensure_resolve(abort=False):
@ -297,6 +298,7 @@ def resume_dialog(resume):
# "Resume from {0:s}"
# "Start from beginning"
resume = datetime.timedelta(seconds=resume)
LOG.debug('Showing PKC resume dialog for resume: %s', resume)
answ = utils.dialog('contextmenu',
[utils.lang(12022).replace('{0:s}', '{0}').format(unicode(resume)),
utils.lang(12021)])
@ -376,7 +378,7 @@ def _prep_playlist_stack(xml, resume):
'part': part,
'playcount': api.viewcount(),
'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()
})
return stack
@ -413,33 +415,20 @@ def _process_stack(playqueue, stack):
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 _use_kodi_db_offset(plex_id, plex_type, plex_offset):
"""
Do NOT use item.offset directly but get it from the Kodi DB (Plex might not
have gotten the last resume point)
"""
if plex_type not in (v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_EPISODE):
return plex_offset
with PlexDB(lock=False) as plexdb:
db_item = plexdb.item_by_id(plex_id, plex_type)
if db_item:
with KodiVideoDB(lock=False) as kodidb:
return kodidb.get_resume(db_item['kodi_fileid'])
else:
return plex_offset
def _conclude_playback(playqueue, pos):
@ -457,19 +446,14 @@ def _conclude_playback(playqueue, pos):
start playback
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]
if item.xml is not None:
# Got a Plex element
api = API(item.xml)
api.part = item.part or 0
listitem = api.listitem(listitem=transfer.PKCListItem)
set_playurl(api, item)
else:
listitem = transfer.PKCListItem()
api = None
api = API(item.xml)
api.part = item.part or 0
listitem = api.listitem(listitem=transfer.PKCListItem, resume=False)
set_playurl(api, item)
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()
return
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,
v.PLAYBACK_METHOD_TRANSCODE):
audio_subtitle_prefs(api, listitem)
_set_resume(listitem, item, api)
transfer.send(listitem)
LOG.info('Done concluding playback')
LOG.debug('Done concluding playback')
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
setResolvedUrl
"""
LOG.info('process_indirect called with key: %s, offset: %s, resolve: %s',
key, offset, resolve)
LOG.debug('process_indirect called with key: %s, offset: %s, resolve: %s',
key, offset, resolve)
global RESOLVE
RESOLVE = resolve
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
api = API(xml[0])
listitem = api.listitem(listitem=transfer.PKCListItem)
listitem = api.listitem(listitem=transfer.PKCListItem, resume=False)
playqueue = PQ.get_playqueue_from_type(
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type])
playqueue.clear()
@ -552,7 +535,7 @@ def process_indirect(key, offset, resolve=True):
args={'item': utils.try_encode(playurl),
'listitem': listitem})
thread.setDaemon(True)
LOG.info('Done initializing PKC playback, starting Kodi player')
LOG.debug('Done initializing PKC playback, starting Kodi player')
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
playqueue.selectedItemID
"""
offset = int(offset) if offset else None
LOG.info("play_xml called with offset %s, start_plex_id %s",
offset, start_plex_id)
offset = int(offset) / 1000 if offset else None
LOG.debug("play_xml called with offset %s, start_plex_id %s",
offset, start_plex_id)
start_item = start_plex_id if start_plex_id is not None \
else playqueue.selectedItemID
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)
thread = Thread(target=threaded_playback,
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()
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)
if offset and offset != '0':
i = 0
while not app.APP.is_playing or not js.get_player_ids():
app.APP.monitor.waitForAbort(0.1)
i += 1
if i > 100:
LOG.error('Could not seek to %s', offset)
return
js.seek_to(int(offset))
offset = offset if offset else 0
i = 0
while not app.APP.is_playing or not js.get_player_ids():
if app.APP.monitor.waitForAbort(0.1):
# PKC needs to quit
return
i += 1
if i > 200:
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)

View file

@ -6,7 +6,7 @@ from requests import exceptions
from .downloadutils import DownloadUtils as DU
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')

View file

@ -37,10 +37,15 @@ class PlaybackTask(backgroundthread.Task):
resolve = False if params.get('handle') == '-1' else True
LOG.debug('Received mode: %s, params: %s', mode, params)
if mode == 'play':
if params.get('resume'):
resume = params.get('resume') == '1'
else:
resume = None
playback.playback_triage(plex_id=params.get('plex_id'),
plex_type=params.get('plex_type'),
path=params.get('path'),
resolve=resolve)
resolve=resolve,
resume=resume)
elif mode == 'plex_node':
playback.process_indirect(params['key'],
params['offset'],

View file

@ -213,6 +213,7 @@ class PlaylistItem(object):
"'guid': '{self.guid}', "
"'playmethod': '{self.playmethod}', "
"'playcount': {self.playcount}, "
"'resume': {self.resume},"
"'offset': {self.offset}, "
"'force_transcode': {self.force_transcode}, "
"'part': {self.part}".format(self=self))

View file

@ -64,7 +64,11 @@ def _m3u_iterator(text):
lines = iter(text.split('\n'))
for line in lines:
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):

View file

@ -57,6 +57,12 @@ class Base(object):
"""
return self.xml.tag
def tag_label(self):
"""
Returns the 'tag' attribute of the xml
"""
return self.xml.get('tag')
@property
def attrib(self):
"""
@ -605,11 +611,16 @@ class Base(object):
% (v.ADDON_ID, url, v.PLEX_TYPE_CLIP))
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
Pass resume=False in order to NOT set a resume point (but let Kodi
automatically handle it)
"""
item = widgets.generate_item(self)
if not resume and 'resume' in item:
del item['resume']
item = widgets.prepare_listitem(item)
return widgets.create_listitem(item, as_tuple=False, listitem=listitem)

View file

@ -2,7 +2,6 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals
from ..utils import cast
from .. import utils, variables as v, app

View file

@ -59,10 +59,8 @@ def update_playqueue_from_PMS(playqueue,
# reconnects and Kodi is already playing something - silly, really
# For all other cases, a new playqueue is generated by Plex
LOG.debug('Update for existing playqueue detected')
new = False
else:
new = True
playqueue.clear()
return
playqueue.clear()
# Get new metadata for the playqueue first
try:
PL.get_playlist_details_from_xml(playqueue, xml)
@ -71,33 +69,10 @@ def update_playqueue_from_PMS(playqueue,
return
playqueue.repeat = 0 if not repeat else int(repeat)
playqueue.plex_transient_token = transient_token
if new:
playback.play_xml(playqueue,
xml,
offset=offset,
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))
playback.play_xml(playqueue,
xml,
offset=offset,
start_plex_id=start_plex_id)
class PlexCompanion(backgroundthread.KillableThread):
@ -151,11 +126,10 @@ class PlexCompanion(backgroundthread.KillableThread):
playback.play_xml(playqueue, xml, offset)
else:
app.CONN.plex_transient_token = data.get('token')
if data.get('offset') != '0':
app.PLAYSTATE.resume_playback = True
playback.playback_triage(api.plex_id,
api.plex_type,
resolve=False)
resolve=False,
resume=data.get('offset') not in ('0', None))
@staticmethod
def _process_node(data):

View file

@ -95,7 +95,6 @@ class Service(object):
self.setup = None
self.alexa = None
self.playqueue = None
self.context_monitor = None
# Flags for other threads
self.connection_check_running = False
self.auth_running = False
@ -422,9 +421,6 @@ class Service(object):
# Some plumbing
app.init()
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()
# Initialize the PKC playqueues
PQ.init_playqueues()

View file

@ -63,25 +63,21 @@ def kodi_now():
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],
'minutes': [int],
'seconds'[int],
'milliseconds': [int]
}
Pass in the time in milliseconds as an int
"""
seconds = int(milliseconds / 1000)
minutes = int(seconds / 60)
seconds = seconds % 60
hours = int(minutes / 60)
minutes = minutes % 60
milliseconds = milliseconds % 1000
return {'hours': hours,
'minutes': minutes,
'seconds': seconds,
'milliseconds': milliseconds}
return {'hours': int(minutes / 60),
'minutes': int(minutes % 60),
'seconds': int(seconds % 60),
'milliseconds': int(milliseconds % 1000)}
def kodi_time_to_millis(time):

View file

@ -181,6 +181,9 @@ PLEX_TYPE_PHOTO = 'photo'
PLEX_TYPE_PLAYLIST = 'playlist'
PLEX_TYPE_CHANNEL = 'channel'
# E.g. PMS answer when hitting the PMS endpoint /hubs/search
PLEX_TYPE_TAG = 'tag'
# Used for /:/timeline XML messages
PLEX_PLAYLIST_TYPE_VIDEO = 'video'
PLEX_PLAYLIST_TYPE_AUDIO = 'music'

View file

@ -105,9 +105,10 @@ def _generate_folder(api):
return content
else:
art = api.artwork()
title = api.title() if api.plex_type != v.PLEX_TYPE_TAG else api.tag_label()
return {
'title': api.title(),
'label': api.title(),
'title': title,
'label': title,
'file': api.directory_path(section_id=SECTION_ID,
plex_type=PLEX_TYPE,
old_key=KEY),