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) [![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-)

View file

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

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.11:
- version 2.9.10 for everyone - version 2.9.10 for everyone

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

@ -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'],

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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