Compare commits
3 Commits
e01b6b5c81
...
869e3a5482
Author | SHA1 | Date | |
---|---|---|---|
869e3a5482 | |||
97d80f2115 | |||
|
0ddcdb1b0b |
@ -1,5 +1,13 @@
|
||||
# Changelog
|
||||
|
||||
[Full Changelog](https://git.jeroened.be/JeroenED/plugin.video.viervijfzes/compare/v0.4.11...v0.4.12)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Add live channels [\#129](https://github.com/add-ons/plugin.video.viervijfzes/pull/129) ([mediaminister](https://github.com/mediaminister))
|
||||
- Add PlayCrime Channel
|
||||
|
||||
|
||||
## [v0.4.11](https://github.com/add-ons/plugin.video.viervijfzes/tree/v0.4.11) (2023-07-26)
|
||||
|
||||
[Full Changelog](https://github.com/add-ons/plugin.video.viervijfzes/compare/v0.4.10...v0.4.11)
|
||||
|
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<addon id="plugin.video.viervijfzes" name="GoPlay" version="0.4.11" provider-name="Michaël Arnauts">
|
||||
<addon id="plugin.video.viervijfzes" name="GoPlay" version="0.4.12" provider-name="Michaël Arnauts">
|
||||
<requires>
|
||||
<import addon="xbmc.python" version="3.0.0"/>
|
||||
<import addon="script.module.dateutil" version="2.6.0"/>
|
||||
@ -21,8 +21,9 @@
|
||||
<disclaimer lang="en_GB">This add-on is not officially commissioned/supported by SBS Belgium and is provided 'as is' without any warranty of any kind. The Play4, Play5 and Play6 logos are property of SBS Belgium.</disclaimer>
|
||||
<platform>all</platform>
|
||||
<license>GPL-3.0-only</license>
|
||||
<news>v0.4.11 (2023-07-26)
|
||||
- Fixes for DRM protected VOD.</news>
|
||||
<news>v0.4.12 (2024-05-08)
|
||||
- Added live channels (by mediaminister)
|
||||
- Added PlayCrime Channel</news>
|
||||
<source>https://github.com/add-ons/plugin.video.viervijfzes</source>
|
||||
<assets>
|
||||
<icon>resources/icon.png</icon>
|
||||
|
@ -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 ""
|
||||
|
@ -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]"
|
||||
|
@ -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>')
|
||||
|
@ -71,9 +71,27 @@ class Channels:
|
||||
|
||||
# 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(
|
||||
|
@ -183,7 +183,7 @@ class Menu:
|
||||
|
||||
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=''))
|
||||
|
@ -26,8 +26,7 @@ class Player:
|
||||
# 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
|
||||
"""
|
||||
@ -38,9 +37,9 @@ class Player:
|
||||
# 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.
|
||||
@ -69,7 +68,7 @@ class Player:
|
||||
|
||||
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:
|
||||
@ -81,24 +80,24 @@ class Player:
|
||||
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
|
||||
@ -115,7 +114,7 @@ class Player:
|
||||
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:
|
||||
|
@ -7,6 +7,7 @@ from collections import OrderedDict
|
||||
CHANNELS = OrderedDict([
|
||||
('Play4', {
|
||||
'name': 'Play4',
|
||||
'url': 'live-kijken/play-4',
|
||||
'epg_id': 'vier',
|
||||
'logo': 'play4.png',
|
||||
'background': 'play4-background.png',
|
||||
@ -18,6 +19,7 @@ CHANNELS = OrderedDict([
|
||||
}),
|
||||
('Play5', {
|
||||
'name': 'Play5',
|
||||
'url': 'live-kijken/play-5',
|
||||
'epg_id': 'vijf',
|
||||
'logo': 'play5.png',
|
||||
'background': 'play5-background.png',
|
||||
@ -29,6 +31,7 @@ CHANNELS = OrderedDict([
|
||||
}),
|
||||
('Play6', {
|
||||
'name': 'Play6',
|
||||
'url': 'live-kijken/play-6',
|
||||
'epg_id': 'zes',
|
||||
'logo': 'play6.png',
|
||||
'background': 'play6-background.png',
|
||||
@ -40,14 +43,24 @@ CHANNELS = OrderedDict([
|
||||
}),
|
||||
('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,
|
||||
'iptv_id': 'play7.be',
|
||||
'youtube': []
|
||||
}),
|
||||
('PlayCrime', {
|
||||
'name': 'PlayCrime',
|
||||
'url': 'live-kijken/play-crime',
|
||||
'epg_id': 'crime',
|
||||
'logo': 'playcrime.png',
|
||||
'background': 'playcrime-background.png',
|
||||
'iptv_preset': 18,
|
||||
'iptv_id': 'playcrime.be',
|
||||
'youtube': []
|
||||
}),
|
||||
('GoPlay', {
|
||||
'name': 'Go Play',
|
||||
'url': 'https://www.goplay.be',
|
||||
|
@ -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
|
||||
@ -130,7 +130,7 @@ class Episode:
|
||||
:type aired: datetime
|
||||
:type expiry: datetime
|
||||
:type stream: string
|
||||
:type islongform: bool
|
||||
:type content_type: string
|
||||
"""
|
||||
self.uuid = uuid
|
||||
self.nodeid = nodeid
|
||||
@ -148,7 +148,7 @@ class Episode:
|
||||
self.aired = aired
|
||||
self.expiry = expiry
|
||||
self.stream = stream
|
||||
self.islongform = islongform
|
||||
self.content_type = content_type
|
||||
|
||||
def __repr__(self):
|
||||
return "%r" % self.__dict__
|
||||
@ -338,6 +338,14 @@ class ContentApi:
|
||||
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'])
|
||||
@ -353,14 +361,19 @@ class ContentApi:
|
||||
|
||||
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:
|
||||
@ -482,8 +495,8 @@ class ContentApi:
|
||||
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):
|
||||
@ -492,9 +505,9 @@ class ContentApi:
|
||||
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
|
||||
@ -547,8 +560,8 @@ class ContentApi:
|
||||
: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
|
||||
@ -574,20 +587,21 @@ class ContentApi:
|
||||
: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:
|
||||
@ -632,6 +646,8 @@ class ContentApi:
|
||||
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('/'),
|
||||
@ -642,6 +658,7 @@ class ContentApi:
|
||||
uuid=episode_video_id,
|
||||
thumb=episode_image,
|
||||
program_title=episode_program,
|
||||
content_type=content_type
|
||||
))
|
||||
|
||||
return episodes
|
||||
@ -721,7 +738,7 @@ class ContentApi:
|
||||
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
|
||||
|
||||
|
BIN
resources/logos/playcrime-background.png
Normal file
BIN
resources/logos/playcrime-background.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 44 KiB |
BIN
resources/logos/playcrime.png
Normal file
BIN
resources/logos/playcrime.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
Loading…
Reference in New Issue
Block a user