Add live channels
This commit is contained in:
parent
e01b6b5c81
commit
0ddcdb1b0b
@ -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 ""
|
||||||
|
@ -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]"
|
||||||
|
@ -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>')
|
||||||
|
@ -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(
|
||||||
|
@ -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=''))
|
||||||
|
@ -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:
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user