Add live channels

This commit is contained in:
mediaminister 2023-09-21 09:52:57 +02:00
parent e01b6b5c81
commit 0ddcdb1b0b
8 changed files with 84 additions and 42 deletions

View File

@ -60,6 +60,10 @@ msgstr ""
### SUBMENUS ### SUBMENUS
msgctxt "#30052"
msgid "Watch live [B]{channel}[/B]"
msgstr ""
msgctxt "#30053" msgctxt "#30053"
msgid "TV Guide for [B]{channel}[/B]" msgid "TV Guide for [B]{channel}[/B]"
msgstr "" msgstr ""

View File

@ -61,6 +61,10 @@ msgstr "Tv-gids"
### SUBMENUS ### SUBMENUS
msgctxt "#30052"
msgid "Watch live [B]{channel}[/B]"
msgstr "Kijk live [B]{channel}[/B]"
msgctxt "#30053" msgctxt "#30053"
msgid "TV Guide for [B]{channel}[/B]" msgid "TV Guide for [B]{channel}[/B]"
msgstr "Tv-gids voor [B]{channel}[/B]" msgstr "Tv-gids voor [B]{channel}[/B]"

View File

@ -160,14 +160,11 @@ def play_epg(channel, timestamp):
@routing.route('/play/catalog') @routing.route('/play/catalog')
@routing.route('/play/catalog/<uuid>') @routing.route('/play/catalog/<uuid>/<content_type>')
@routing.route('/play/catalog/<uuid>/<islongform>') def play_catalog(uuid=None, content_type=None):
def play_catalog(uuid=None, islongform=False):
""" Play the requested item """ """ Play the requested item """
from ast import literal_eval
from resources.lib.modules.player import Player from resources.lib.modules.player import Player
# Convert string to bool using literal_eval Player().play(uuid, content_type)
Player().play(uuid, literal_eval(islongform))
@routing.route('/play/page/<page>') @routing.route('/play/page/<page>')

View File

@ -71,9 +71,27 @@ class Channels:
# Lookup the high resolution logo based on the channel name # Lookup the high resolution logo based on the channel name
fanart = '{path}/resources/logos/{logo}'.format(path=kodiutils.addon_path(), logo=channel_info.get('background')) fanart = '{path}/resources/logos/{logo}'.format(path=kodiutils.addon_path(), logo=channel_info.get('background'))
icon = '{path}/resources/logos/{logo}'.format(path=kodiutils.addon_path(), logo=channel_info.get('logo'))
listing = [] listing = []
listing.append(
TitleItem(
title=kodiutils.localize(30052, channel=channel_info.get('name')), # Watch live {channel}
path=kodiutils.url_for('play_live', channel=channel_info.get('name')) + '?.pvr',
art_dict={
'icon': icon,
'fanart': fanart,
},
info_dict={
'plot': kodiutils.localize(30052, channel=channel_info.get('name')), # Watch live {channel}
'playcount': 0,
'mediatype': 'video',
},
is_playable=True,
)
)
if channel_info.get('epg_id'): if channel_info.get('epg_id'):
listing.append( listing.append(
TitleItem( TitleItem(

View File

@ -183,7 +183,7 @@ class Menu:
if item.uuid: if item.uuid:
# We have an UUID and can play this item directly # We have an UUID and can play this item directly
path = kodiutils.url_for('play_catalog', uuid=item.uuid, islongform=item.islongform) path = kodiutils.url_for('play_catalog', uuid=item.uuid, content_type=item.content_type)
else: else:
# We don't have an UUID, and first need to fetch the video information from the page # We don't have an UUID, and first need to fetch the video information from the page
path = kodiutils.url_for('play_from_page', page=quote(item.path, safe='')) path = kodiutils.url_for('play_from_page', page=quote(item.path, safe=''))

View File

@ -26,8 +26,7 @@ class Player:
# Workaround for Raspberry Pi 3 and older # Workaround for Raspberry Pi 3 and older
kodiutils.set_global_setting('videoplayer.useomxplayer', True) kodiutils.set_global_setting('videoplayer.useomxplayer', True)
@staticmethod def live(self, channel):
def live(channel):
""" Play the live channel. """ Play the live channel.
:type channel: string :type channel: string
""" """
@ -38,9 +37,9 @@ class Player:
# self.play_from_page(broadcast.video_url) # self.play_from_page(broadcast.video_url)
# return # return
channel_name = CHANNELS.get(channel, {'name': channel}) channel_url = CHANNELS.get(channel, {'url': channel}).get('url')
kodiutils.ok_dialog(message=kodiutils.localize(30718, channel=channel_name.get('name'))) # There is no live stream available for {channel}.
kodiutils.end_of_directory() self.play_from_page(channel_url)
def play_from_page(self, path): def play_from_page(self, path):
""" Play the requested item. """ Play the requested item.
@ -69,7 +68,7 @@ class Player:
if episode.uuid: if episode.uuid:
# Lookup the stream # Lookup the stream
resolved_stream = self._resolve_stream(episode.uuid, episode.islongform) resolved_stream = self._resolve_stream(episode.uuid, episode.content_type)
_LOGGER.debug('Resolved stream: %s', resolved_stream) _LOGGER.debug('Resolved stream: %s', resolved_stream)
if resolved_stream: if resolved_stream:
@ -81,24 +80,24 @@ class Player:
art_dict=titleitem.art_dict, art_dict=titleitem.art_dict,
prop_dict=titleitem.prop_dict) prop_dict=titleitem.prop_dict)
def play(self, uuid, islongform): def play(self, uuid, content_type):
""" Play the requested item. """ Play the requested item.
:type uuid: string :type uuid: string
:type islongform: bool :type content_type: string
""" """
if not uuid: if not uuid:
kodiutils.ok_dialog(message=kodiutils.localize(30712)) # The video is unavailable... kodiutils.ok_dialog(message=kodiutils.localize(30712)) # The video is unavailable...
return return
# Lookup the stream # Lookup the stream
resolved_stream = self._resolve_stream(uuid, islongform) resolved_stream = self._resolve_stream(uuid, content_type)
kodiutils.play(resolved_stream.url, resolved_stream.stream_type, resolved_stream.license_key) kodiutils.play(resolved_stream.url, resolved_stream.stream_type, resolved_stream.license_key)
@staticmethod @staticmethod
def _resolve_stream(uuid, islongform): def _resolve_stream(uuid, content_type):
""" Resolve the stream for the requested item """ Resolve the stream for the requested item
:type uuid: string :type uuid: string
:type islongform: bool :type content_type: string
""" """
try: try:
# Check if we have credentials # Check if we have credentials
@ -115,7 +114,7 @@ class Player:
auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password'), kodiutils.get_tokens_path()) auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password'), kodiutils.get_tokens_path())
# Get stream information # Get stream information
resolved_stream = ContentApi(auth).get_stream_by_uuid(uuid, islongform) resolved_stream = ContentApi(auth).get_stream_by_uuid(uuid, content_type)
return resolved_stream return resolved_stream
except (InvalidLoginException, AuthenticationException) as ex: except (InvalidLoginException, AuthenticationException) as ex:

View File

@ -7,6 +7,7 @@ from collections import OrderedDict
CHANNELS = OrderedDict([ CHANNELS = OrderedDict([
('Play4', { ('Play4', {
'name': 'Play4', 'name': 'Play4',
'url': 'live-kijken/play-4',
'epg_id': 'vier', 'epg_id': 'vier',
'logo': 'play4.png', 'logo': 'play4.png',
'background': 'play4-background.png', 'background': 'play4-background.png',
@ -18,6 +19,7 @@ CHANNELS = OrderedDict([
}), }),
('Play5', { ('Play5', {
'name': 'Play5', 'name': 'Play5',
'url': 'live-kijken/play-5',
'epg_id': 'vijf', 'epg_id': 'vijf',
'logo': 'play5.png', 'logo': 'play5.png',
'background': 'play5-background.png', 'background': 'play5-background.png',
@ -29,6 +31,7 @@ CHANNELS = OrderedDict([
}), }),
('Play6', { ('Play6', {
'name': 'Play6', 'name': 'Play6',
'url': 'live-kijken/play-6',
'epg_id': 'zes', 'epg_id': 'zes',
'logo': 'play6.png', 'logo': 'play6.png',
'background': 'play6-background.png', 'background': 'play6-background.png',
@ -40,8 +43,8 @@ CHANNELS = OrderedDict([
}), }),
('Play7', { ('Play7', {
'name': 'Play7', 'name': 'Play7',
'url': 'live-kijken/play-7',
'epg_id': 'zeven', 'epg_id': 'zeven',
'url': 'https://www.goplay.be',
'logo': 'play7.png', 'logo': 'play7.png',
'background': 'play7-background.png', 'background': 'play7-background.png',
'iptv_preset': 17, 'iptv_preset': 17,

View File

@ -112,7 +112,7 @@ class Episode:
""" Defines an Episode. """ """ Defines an Episode. """
def __init__(self, uuid=None, nodeid=None, path=None, channel=None, program_title=None, title=None, description=None, thumb=None, duration=None, def __init__(self, uuid=None, nodeid=None, path=None, channel=None, program_title=None, title=None, description=None, thumb=None, duration=None,
season=None, season_uuid=None, number=None, rating=None, aired=None, expiry=None, stream=None, islongform=False): season=None, season_uuid=None, number=None, rating=None, aired=None, expiry=None, stream=None, content_type=None):
""" """
:type uuid: str :type uuid: str
:type nodeid: str :type nodeid: str
@ -130,7 +130,7 @@ class Episode:
:type aired: datetime :type aired: datetime
:type expiry: datetime :type expiry: datetime
:type stream: string :type stream: string
:type islongform: bool :type content_type: string
""" """
self.uuid = uuid self.uuid = uuid
self.nodeid = nodeid self.nodeid = nodeid
@ -148,7 +148,7 @@ class Episode:
self.aired = aired self.aired = aired
self.expiry = expiry self.expiry = expiry
self.stream = stream self.stream = stream
self.islongform = islongform self.content_type = content_type
def __repr__(self): def __repr__(self):
return "%r" % self.__dict__ return "%r" % self.__dict__
@ -338,6 +338,14 @@ class ContentApi:
if not data: if not data:
return None return None
if 'episode' in data and data['episode']['pageInfo']['type'] == 'live_channel':
episode = Episode(
uuid=data['episode']['pageInfo']['nodeUuid'],
program_title=data['episode']['pageInfo']['title'],
content_type=data['episode']['pageInfo']['type'],
)
return episode
if 'video' in data and data['video']: if 'video' in data and data['video']:
# We have found detailed episode information # We have found detailed episode information
episode = self._parse_clip_data(data['video']) episode = self._parse_clip_data(data['video'])
@ -353,14 +361,19 @@ class ContentApi:
return None return None
def get_stream_by_uuid(self, uuid, islongform): def get_stream_by_uuid(self, uuid, content_type):
""" Return a ResolvedStream for this video. """ Return a ResolvedStream for this video.
:type uuid: str :type uuid: string
:type islongform: bool :type content_type: string
:rtype: ResolvedStream :rtype: ResolvedStream
""" """
mode = 'long-form' if islongform else 'short-form' if content_type in ('video-long_form', 'long_form'):
response = self._get_url(self.API_GOPLAY + '/web/v1/videos/%s/%s' % (mode, uuid), authentication='Bearer %s' % self._auth.get_token()) mode = 'videos/long-form'
elif content_type == 'video-short_form':
mode = 'videos/short-form'
elif content_type == 'live_channel':
mode = 'liveStreams'
response = self._get_url(self.API_GOPLAY + '/web/v1/%s/%s' % (mode, uuid), authentication='Bearer %s' % self._auth.get_token())
data = json.loads(response) data = json.loads(response)
if not data: if not data:
@ -482,8 +495,8 @@ class ContentApi:
raw_html = self._get_url(self.SITE_URL) raw_html = self._get_url(self.SITE_URL)
# Categories regexes # Categories regexes
regex_articles = re.compile(r'<article[^>]+>(.*?)</article>', re.DOTALL) regex_articles = re.compile(r'<article[^>]+>([\s\S]*?)</article>', re.DOTALL)
regex_category = re.compile(r'<h2.*?>(.*?)</h2>(?:.*?<div class="visually-hidden">(.*?)</div>)?', re.DOTALL) regex_category = re.compile(r'<h2.*?>(.*?)</h2>(?:.*?<div class=\"visually-hidden\">(.*?)</div>)?', re.DOTALL)
categories = [] categories = []
for result in regex_articles.finditer(raw_html): for result in regex_articles.finditer(raw_html):
@ -492,9 +505,9 @@ class ContentApi:
match_category = regex_category.search(article_html) match_category = regex_category.search(article_html)
category_title = None category_title = None
if match_category: if match_category:
category_title = match_category.group(1).strip() category_title = unescape(match_category.group(1).strip())
if match_category.group(2): if match_category.group(2):
category_title += ' [B]%s[/B]' % match_category.group(2).strip() category_title += ' [B]%s[/B]' % unescape(match_category.group(2).strip())
if category_title: if category_title:
# Extract programs and lookup in all_programs so we have more metadata # Extract programs and lookup in all_programs so we have more metadata
@ -547,8 +560,8 @@ class ContentApi:
:rtype list[Program] :rtype list[Program]
""" """
# Item regexes # Item regexes
regex_item = re.compile(r'<a[^>]+?href="(?P<path>[^"]+)"[^>]+?>' regex_item = re.compile(r'<a[^>]+?href=\"(?P<path>[^\"]+)\"[^>]+?>'
r'.*?<h3 class="poster-teaser__title">(?P<title>[^<]*)</h3>.*?data-background-image="(?P<image>.*?)".*?' r'[\s\S]*?<h3 class=\"poster-teaser__title\">(?P<title>[^<]*)</h3>[\s\S]*?poster-teaser__image\" src=\"(?P<image>[\s\S]*?)\"[\s\S]*?'
r'</a>', re.DOTALL) r'</a>', re.DOTALL)
# Extract items # Extract items
@ -574,20 +587,21 @@ class ContentApi:
:rtype list[Episode] :rtype list[Episode]
""" """
# Item regexes # Item regexes
regex_item = re.compile(r'<a[^>]+?href="(?P<path>[^"]+)"[^>]+?>.*?</a>', re.DOTALL) regex_item = re.compile(r'<a[^>]+?class=\"(?P<item_type>[^\"]+)\"[^>]+?href=\"(?P<path>[^\"]+)\"[^>]+?>[\s\S]*?</a>', re.DOTALL)
regex_episode_program = re.compile(r'<h3 class="episode-teaser__subtitle">([^<]*)</h3>') regex_episode_program = re.compile(r'<(?:div|h3) class=\"episode-teaser__subtitle\">([^<]*)</(?:div|h3)>')
regex_episode_title = re.compile(r'<(?:div|h3) class="(?:poster|card|image|episode)-teaser__title">(?:<span>)?([^<]*)(?:</span>)?</(?:div|h3)>') regex_episode_title = re.compile(r'<(?:div|h3) class=\"(?:poster|card|image|episode)-teaser__title\">(?:<span>)?([^<]*)(?:</span>)?</(?:div|h3)>')
regex_episode_duration = re.compile(r'data-duration="([^"]*)"') regex_episode_duration = re.compile(r'data-duration=\"([^\"]*)\"')
regex_episode_video_id = re.compile(r'data-video-id="([^"]*)"') regex_episode_video_id = re.compile(r'data-video-id=\"([^\"]*)\"')
regex_episode_image = re.compile(r'data-background-image="([^"]*)"') regex_episode_image = re.compile(r'<img class=\"episode-teaser__header\" src=\"([^<\"]*)\"')
regex_episode_badge = re.compile(r'<div class="(?:poster|card|image|episode)-teaser__badge badge">([^<]*)</div>') regex_episode_badge = re.compile(r'<div class=\"badge (?:poster|card|image|episode)-teaser__badge (?:poster|card|image|episode)-teaser__badge--default\">([^<]*)</div>')
# Extract items # Extract items
episodes = [] episodes = []
for item in regex_item.finditer(html): for item in regex_item.finditer(html):
item_html = item.group(0) item_html = item.group(0)
path = item.group('path') path = item.group('path')
item_type = item.group('item_type')
# Extract title # Extract title
try: try:
@ -632,6 +646,8 @@ class ContentApi:
if episode_badge: if episode_badge:
description += "\n\n[B]%s[/B]" % episode_badge description += "\n\n[B]%s[/B]" % episode_badge
content_type = 'video-short_form' if 'card-' in item_type else 'video-long_form'
# Episode # Episode
episodes.append(Episode( episodes.append(Episode(
path=path.lstrip('/'), path=path.lstrip('/'),
@ -642,6 +658,7 @@ class ContentApi:
uuid=episode_video_id, uuid=episode_video_id,
thumb=episode_image, thumb=episode_image,
program_title=episode_program, program_title=episode_program,
content_type=content_type
)) ))
return episodes return episodes
@ -721,7 +738,7 @@ class ContentApi:
expiry=datetime.fromtimestamp(int(data.get('unpublishDate'))) if data.get('unpublishDate') else None, expiry=datetime.fromtimestamp(int(data.get('unpublishDate'))) if data.get('unpublishDate') else None,
rating=data.get('parentalRating'), rating=data.get('parentalRating'),
stream=data.get('path'), stream=data.get('path'),
islongform=data.get('isLongForm'), content_type=data.get('type'),
) )
return episode return episode