Skip to content
This repository has been archived by the owner on Oct 21, 2024. It is now read-only.

Add live channels #129

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions resources/language/resource.language.en_gb/strings.po
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ msgstr ""


### SUBMENUS
msgctxt "#30052"
msgid "Watch live [B]{channel}[/B]"
msgstr ""

msgctxt "#30053"
msgid "TV Guide for [B]{channel}[/B]"
msgstr ""
Expand Down
4 changes: 4 additions & 0 deletions resources/language/resource.language.nl_nl/strings.po
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ msgstr "Tv-gids"


### SUBMENUS
msgctxt "#30052"
msgid "Watch live [B]{channel}[/B]"
msgstr "Kijk live [B]{channel}[/B]"

msgctxt "#30053"
msgid "TV Guide for [B]{channel}[/B]"
msgstr "Tv-gids voor [B]{channel}[/B]"
Expand Down
9 changes: 3 additions & 6 deletions resources/lib/addon.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,14 +160,11 @@ def play_epg(channel, timestamp):


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


@routing.route('/play/page/<page>')
Expand Down
18 changes: 18 additions & 0 deletions resources/lib/modules/channels.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,27 @@ def show_channel_menu(channel):

# 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'))
icon = '{path}/resources/logos/{logo}'.format(path=kodiutils.addon_path(), logo=channel_info.get('logo'))

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'):
listing.append(
TitleItem(
Expand Down
2 changes: 1 addition & 1 deletion resources/lib/modules/menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ def generate_titleitem(item):

if item.uuid:
# 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:
# 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=''))
Expand Down
23 changes: 11 additions & 12 deletions resources/lib/modules/player.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ def __init__(self):
# Workaround for Raspberry Pi 3 and older
kodiutils.set_global_setting('videoplayer.useomxplayer', True)

@staticmethod
def live(channel):
def live(self, channel):
""" Play the live channel.
:type channel: string
"""
Expand All @@ -38,9 +37,9 @@ def live(channel):
# self.play_from_page(broadcast.video_url)
# return

channel_name = CHANNELS.get(channel, {'name': channel})
kodiutils.ok_dialog(message=kodiutils.localize(30718, channel=channel_name.get('name'))) # There is no live stream available for {channel}.
kodiutils.end_of_directory()
channel_url = CHANNELS.get(channel, {'url': channel}).get('url')

self.play_from_page(channel_url)

def play_from_page(self, path):
""" Play the requested item.
Expand Down Expand Up @@ -69,7 +68,7 @@ def play_from_page(self, path):

if episode.uuid:
# 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)

if resolved_stream:
Expand All @@ -81,24 +80,24 @@ def play_from_page(self, path):
art_dict=titleitem.art_dict,
prop_dict=titleitem.prop_dict)

def play(self, uuid, islongform):
def play(self, uuid, content_type):
""" Play the requested item.
:type uuid: string
:type islongform: bool
:type content_type: string
"""
if not uuid:
kodiutils.ok_dialog(message=kodiutils.localize(30712)) # The video is unavailable...
return

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

@staticmethod
def _resolve_stream(uuid, islongform):
def _resolve_stream(uuid, content_type):
""" Resolve the stream for the requested item
:type uuid: string
:type islongform: bool
:type content_type: string
"""
try:
# Check if we have credentials
Expand All @@ -115,7 +114,7 @@ def _resolve_stream(uuid, islongform):
auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password'), kodiutils.get_tokens_path())

# 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

except (InvalidLoginException, AuthenticationException) as ex:
Expand Down
5 changes: 4 additions & 1 deletion resources/lib/viervijfzes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
CHANNELS = OrderedDict([
('Play4', {
'name': 'Play4',
'url': 'live-kijken/play-4',
'epg_id': 'vier',
'logo': 'play4.png',
'background': 'play4-background.png',
Expand All @@ -18,6 +19,7 @@
}),
('Play5', {
'name': 'Play5',
'url': 'live-kijken/play-5',
'epg_id': 'vijf',
'logo': 'play5.png',
'background': 'play5-background.png',
Expand All @@ -29,6 +31,7 @@
}),
('Play6', {
'name': 'Play6',
'url': 'live-kijken/play-6',
'epg_id': 'zes',
'logo': 'play6.png',
'background': 'play6-background.png',
Expand All @@ -40,8 +43,8 @@
}),
('Play7', {
'name': 'Play7',
'url': 'live-kijken/play-7',
'epg_id': 'zeven',
'url': 'https://www.goplay.be',
'logo': 'play7.png',
'background': 'play7-background.png',
'iptv_preset': 17,
Expand Down
61 changes: 39 additions & 22 deletions resources/lib/viervijfzes/content.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ class 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,
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 nodeid: str
Expand All @@ -130,7 +130,7 @@ def __init__(self, uuid=None, nodeid=None, path=None, channel=None, program_titl
:type aired: datetime
:type expiry: datetime
:type stream: string
:type islongform: bool
:type content_type: string
"""
self.uuid = uuid
self.nodeid = nodeid
Expand All @@ -148,7 +148,7 @@ def __init__(self, uuid=None, nodeid=None, path=None, channel=None, program_titl
self.aired = aired
self.expiry = expiry
self.stream = stream
self.islongform = islongform
self.content_type = content_type

def __repr__(self):
return "%r" % self.__dict__
Expand Down Expand Up @@ -338,6 +338,14 @@ def update():
if not data:
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']:
# We have found detailed episode information
episode = self._parse_clip_data(data['video'])
Expand All @@ -353,14 +361,19 @@ def update():

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.
:type uuid: str
:type islongform: bool
:type uuid: string
:type content_type: string
:rtype: ResolvedStream
"""
mode = 'long-form' if islongform else 'short-form'
response = self._get_url(self.API_GOPLAY + '/web/v1/videos/%s/%s' % (mode, uuid), authentication='Bearer %s' % self._auth.get_token())
if content_type in ('video-long_form', 'long_form'):
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)

if not data:
Expand Down Expand Up @@ -482,8 +495,8 @@ def get_recommendation_categories(self):
raw_html = self._get_url(self.SITE_URL)

# Categories regexes
regex_articles = re.compile(r'<article[^>]+>(.*?)</article>', re.DOTALL)
regex_category = re.compile(r'<h2.*?>(.*?)</h2>(?:.*?<div class="visually-hidden">(.*?)</div>)?', 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)

categories = []
for result in regex_articles.finditer(raw_html):
Expand All @@ -492,9 +505,9 @@ def get_recommendation_categories(self):
match_category = regex_category.search(article_html)
category_title = None
if match_category:
category_title = match_category.group(1).strip()
category_title = unescape(match_category.group(1).strip())
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:
# Extract programs and lookup in all_programs so we have more metadata
Expand Down Expand Up @@ -547,8 +560,8 @@ def _extract_programs(html):
:rtype list[Program]
"""
# Item regexes
regex_item = re.compile(r'<a[^>]+?href="(?P<path>[^"]+)"[^>]+?>'
r'.*?<h3 class="poster-teaser__title">(?P<title>[^<]*)</h3>.*?data-background-image="(?P<image>.*?)".*?'
regex_item = re.compile(r'<a[^>]+?href=\"(?P<path>[^\"]+)\"[^>]+?>'
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)

# Extract items
Expand All @@ -574,20 +587,21 @@ def _extract_videos(html):
:rtype list[Episode]
"""
# 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_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_video_id = re.compile(r'data-video-id="([^"]*)"')
regex_episode_image = re.compile(r'data-background-image="([^"]*)"')
regex_episode_badge = re.compile(r'<div class="(?:poster|card|image|episode)-teaser__badge badge">([^<]*)</div>')
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_duration = re.compile(r'data-duration=\"([^\"]*)\"')
regex_episode_video_id = re.compile(r'data-video-id=\"([^\"]*)\"')
regex_episode_image = re.compile(r'<img class=\"episode-teaser__header\" src=\"([^<\"]*)\"')
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
episodes = []
for item in regex_item.finditer(html):
item_html = item.group(0)
path = item.group('path')
item_type = item.group('item_type')

# Extract title
try:
Expand Down Expand Up @@ -632,6 +646,8 @@ def _extract_videos(html):
if 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
episodes.append(Episode(
path=path.lstrip('/'),
Expand All @@ -642,6 +658,7 @@ def _extract_videos(html):
uuid=episode_video_id,
thumb=episode_image,
program_title=episode_program,
content_type=content_type
))

return episodes
Expand Down Expand Up @@ -721,7 +738,7 @@ def _parse_episode_data(data, season_uuid=None):
expiry=datetime.fromtimestamp(int(data.get('unpublishDate'))) if data.get('unpublishDate') else None,
rating=data.get('parentalRating'),
stream=data.get('path'),
islongform=data.get('isLongForm'),
content_type=data.get('type'),
)
return episode

Expand Down