Skip to content

Commit

Permalink
Use direct URI to play programs from the EPG in Kodi 18 (#34)
Browse files Browse the repository at this point in the history
* Hide direct url in the title.
* Fix double encoding, add logging
* Remove [CR] since the label is already bold.
* Remove check for empty title
  • Loading branch information
michaelarnauts committed Jun 12, 2020
1 parent 186cadb commit 677d2d4
Show file tree
Hide file tree
Showing 5 changed files with 53 additions and 31 deletions.
19 changes: 14 additions & 5 deletions resources/lib/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,20 @@ def refresh():


def play_from_contextmenu():
"""Play an item from the Context Menu"""
# Fetch selection from Kodi
program = ContextMenu.get_selection()
if program:
ContextMenu.play(program)
"""Play an item from the Context Menu in Kodi 18"""
# Use the direct URI if we have any
stream = ContextMenu.get_direct_uri()
if stream:
_LOGGER.debug('Playing using direct URI: %s', stream)
kodiutils.execute_builtin('PlayMedia', stream)
return

# Construct an URI based on the timestamp of the selection
stream = ContextMenu.get_uri_by_timestamp()
if stream:
_LOGGER.debug('Playing using generated URI: %s', stream)
kodiutils.execute_builtin('PlayMedia', stream)
return


def open_settings():
Expand Down
38 changes: 24 additions & 14 deletions resources/lib/modules/contextmenu.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import json
import logging
import os
import re
import sys
import time
from datetime import datetime, timedelta
Expand All @@ -23,10 +24,21 @@ class ContextMenu:
def __init__(self):
"""Initialise the Context Menu Module"""

@staticmethod
def get_direct_uri():
"""Retrieve a direct URI from the selected ListItem."""
# We use a clever way / ugly hack (pick your choice) to hide the direct stream in Kodi 18.
# Title [COLOR green]•[/COLOR][COLOR vod="plugin://plugin.video.example/play/whatever"][/COLOR]
label = sys.listitem.getLabel() # pylint: disable=no-member
stream = re.search(r'\[COLOR vod="([^"]+)"\]', label)
return stream.group(1) if stream else None

@classmethod
def play(cls, program):
"""Play the selected program."""
_LOGGER.debug('Asked to play %s', program)
def get_uri_by_timestamp(cls):
"""Generate an URI based on the timestamp."""
program = cls._get_selection()
if not program:
return None

# Get a list of addons that can play the selected channel
# We do the lookup based on Channel Name, since that's all we have
Expand All @@ -36,36 +48,36 @@ def play(cls, program):
if kodiutils.yesno_dialog(message=kodiutils.localize(30713)): # The EPG data is not up to date...
from resources.lib.modules.addon import Addon
Addon.refresh(True)
return
return None

if not addons:
# Channel was not found.
_LOGGER.debug('No Add-on was found to play %s', program.get('channel'))
kodiutils.notification(
message=kodiutils.localize(30710, channel=program.get('channel'))) # Could not find an Add-on...
return
return None

if len(addons) == 1:
# Channel has one Add-on. Play it directly.
_LOGGER.debug('One Add-on was found to play %s: %s', program.get('channel'), addons)
cls._play(list(addons.values())[0], program)
return
return cls._format_uri(list(addons.values())[0], program)

# Ask the user to pick an Add-on
_LOGGER.debug('Multiple Add-on were found to play %s: %s', program.get('channel'), addons)
addons_list = list(addons)
ret = kodiutils.select(heading=kodiutils.localize(30711), options=addons_list) # Select an Add-on...
if ret == -1:
_LOGGER.debug('The selection to play an item from %s was canceled', program.get('channel'))
return
return None

cls._play(addons.get(addons_list[ret]), program)
return cls._format_uri(addons.get(addons_list[ret]), program)

@classmethod
def _play(cls, uri, program):
def _format_uri(cls, uri, program):
"""Play the selected program with the specified URI."""
format_params = {}
if '{date}' in uri:
_LOGGER.warning('Using {date} is deprecated. Please use {start}.')
format_params.update({'date': program.get('start').isoformat()})

if '{start}' in uri:
Expand All @@ -81,13 +93,11 @@ def _play(cls, uri, program):
if format_params:
uri = uri.format(**format_params)

_LOGGER.debug('Executing "%s"', uri)
kodiutils.execute_builtin('PlayMedia', uri)
return uri

@classmethod
def get_selection(cls):
def _get_selection(cls):
"""Retrieve information about the selected ListItem."""

# The selected ListItem is available in sys.listitem, but there is not enough data that we can use to know what
# exact item was selected. Therefore, we use xbmc.getInfoLabel(ListItem.xxx), that references the currently
# selected ListItem. This is not always the same as the item where the Context Menu was opened on when the
Expand Down
10 changes: 6 additions & 4 deletions resources/lib/modules/iptvsimple.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,12 +163,14 @@ def write_epg(cls, epg):
for item in epg[key]:
start = dateutil.parser.parse(item.get('start')).strftime('%Y%m%d%H%M%S %z')
stop = dateutil.parser.parse(item.get('stop')).strftime('%Y%m%d%H%M%S %z')
title = item.get('title')
title = item.get('title', '')

# Add an icon ourselves in Kodi 18
if title and item.get('stream') and kodiutils.kodi_version_major() < 19:
# Add [CR] to fix a bug that causes the [/B] to be visible
title = title + ' [COLOR green][B]•[/B][/COLOR][CR]'
if kodiutils.kodi_version_major() < 19 and item.get('stream'):
# We use a clever way to hide the direct URI in the label so Kodi 18 can access the it
title = '%s [COLOR green]•[/COLOR][COLOR vod="%s"][/COLOR]' % (
title, item.get('stream')
)

program = '<programme start="{start}" stop="{stop}" channel="{channel}"{vod}>\n'.format(
start=start,
Expand Down
2 changes: 1 addition & 1 deletion tests/mocks/plugin.video.example/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def send_channels(): # pylint: disable=no-method-argument
preset=1,
stream='plugin://plugin.video.example/play/1',
logo='https://example.com/channel1.png',
vod='plugin://plugin.video.example/play/airdate/{date}'
vod='plugin://plugin.video.example/play/airdate/{start}/{stop}/{duration}'
),
dict(
id='channel2.com',
Expand Down
15 changes: 8 additions & 7 deletions tests/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,18 +68,19 @@ def test_refresh(self):
})

# Get the current selected EPG item
program = ContextMenu.get_selection()
self.assertTrue(program)
self.assertEqual(program.get('duration'), 3600)
self.assertEqual(program.get('channel'), 'Channel 1')
selection = ContextMenu._get_selection() # pylint: disable=protected-access
self.assertTrue(selection)
self.assertEqual(selection.get('duration'), 3600)
self.assertEqual(selection.get('channel'), 'Channel 1')

# Make sure we can detect that playback has started
if os.path.exists('/tmp/playback-started.txt'):
os.unlink('/tmp/playback-started.txt')

with patch('xbmcgui.Dialog.select', return_value=0):
# Try to play it
ContextMenu.play(program)
from resources.lib.functions import play_from_contextmenu
play_from_contextmenu()

# Check that something has played
self.assertTrue(self._wait_for_file('/tmp/playback-started.txt'))
Expand All @@ -88,8 +89,8 @@ def test_refresh(self):
sys.listitem = ListItem(path='pvr://guide/0012/2020-05-24 12:00:00.epg')

# Get the current selected EPG item, but the selected item is wrong.
program = ContextMenu.get_selection()
self.assertIsNone(program)
selection = ContextMenu._get_selection() # pylint: disable=protected-access
self.assertIsNone(selection)

@staticmethod
def _wait_for_file(filename, timeout=10):
Expand Down

0 comments on commit 677d2d4

Please sign in to comment.