Rebranding to GoPlay (#64)

* Support for rebranding to GoPlay
This commit is contained in:
Michaël Arnauts 2021-02-01 08:53:13 +01:00 committed by GitHub
parent 8a2129b894
commit e25ebfd8a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 347 additions and 355 deletions

3
.gitignore vendored
View File

@ -15,3 +15,6 @@ Thumbs.db
# Testing
tests/home/userdata/addon_data
.env
Pipfile
Pipfile.lock

View File

@ -4,18 +4,18 @@
[![License: GPLv3](https://img.shields.io/badge/License-GPLv3-yellow.svg)](https://opensource.org/licenses/GPL-3.0)
[![Contributors](https://img.shields.io/github/contributors/add-ons/plugin.video.viervijfzes.svg)](https://github.com/add-ons/plugin.video.viervijfzes/graphs/contributors)
# VIER-VIJF-ZES Kodi add-on
# GoPlay Kodi add-on
*plugin.video.viervijfzes* is een Kodi add-on om de video-on-demand content van [vier.be](https://www.vier.be/), [vijf.be](https://www.vijf.be/) en [zestv.be](https://www.zestv.be/) te bekijken.
*plugin.video.viervijfzes* is een Kodi add-on om de video-on-demand content van [goplay.be](https://www.goplay.be/) te bekijken.
> Note: Je moet eerst een account aanmaken op één van bovenstaande websites.
> Note: Je moet eerst een account aanmaken.
Meer informatie kan je vinden op de [Wiki pagina](https://github.com/add-ons/plugin.video.viervijfzes/wiki).
## Features
De volgende features worden ondersteund:
* Bekijk on-demand content van VIER, VIJF en ZES
* Bekijk on-demand content van Play4, Play5 en Play6
* Programma's rechtstreeks afspelen vanuit de tv-gids
* Doorzoeken van alle programma's
* Afspelen van gerelateerde Youtube content
@ -34,4 +34,4 @@ De volgende features worden ondersteund:
## Disclaimer
Deze add-on wordt niet ondersteund door SBS Belgium, en wordt aangeboden 'as is', zonder enige garantie.
De logo's van VIER, VIJF en ZES zijn eigendom van SBS België.
De logo's van GoPlay, Play4, Play5 en Play6 zijn eigendom van SBS België.

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="plugin.video.viervijfzes" name="VIER-VIJF-ZES" version="0.3.1" provider-name="Michaël Arnauts">
<addon id="plugin.video.viervijfzes" name="GoPlay" version="0.3.1" provider-name="Michaël Arnauts">
<requires>
<import addon="xbmc.python" version="2.26.0"/>
<import addon="script.module.dateutil" version="2.6.0"/>
@ -14,12 +14,12 @@
</extension>
<extension point="xbmc.service" library="service_entry.py"/>
<extension point="xbmc.addon.metadata">
<summary lang="nl_NL">Bekijk programma's van VIER, VIJF en ZES.</summary>
<description lang="nl_NL">Deze add-on geeft toegang tot de programma's die aangeboden worden op de websites van VIER, VIJF en ZES.</description>
<disclaimer lang="nl_NL">Deze add-on wordt niet ondersteund door SBS België, en wordt aangeboden 'as is', zonder enige garantie. De logo's van VIER, VIJF en ZES zijn eigendom van SBS België.</disclaimer>
<summary lang="en_GB">Watch content from VIER, VIJF and ZES.</summary>
<description lang="en_GB">This add-on gives access to video-on-demand content available on the websites of VIER, VIJF and ZES.</description>
<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 VIER, VIJF and ZES logos are property of SBS Belgium.</disclaimer>
<summary lang="nl_NL">Bekijk programma's van Play4, Play5 en Play6.</summary>
<description lang="nl_NL">Deze add-on geeft toegang tot de programma's die aangeboden worden op de websites van Play4, Play5 en Play6.</description>
<disclaimer lang="nl_NL">Deze add-on wordt niet ondersteund door SBS België, en wordt aangeboden 'as is', zonder enige garantie. De logo's van Play4, Play5 en Play6 zijn eigendom van SBS België.</disclaimer>
<summary lang="en_GB">Watch content from Play4, Play5 and Play6.</summary>
<description lang="en_GB">This add-on gives access to video-on-demand content available on the websites of Play4, Play5 and Play6.</description>
<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.3.1 (2020-11-28)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -35,18 +35,18 @@ def show_channel_menu(channel):
Channels().show_channel_menu(channel)
@routing.route('/channels/<channel>/categories')
def show_channel_categories(channel):
""" Shows TV Channel categories """
from resources.lib.modules.channels import Channels
Channels().show_channel_categories(channel)
# @routing.route('/channels/<channel>/categories')
# def show_channel_categories(channel):
# """ Shows TV Channel categories """
# from resources.lib.modules.channels import Channels
# Channels().show_channel_categories(channel)
@routing.route('/channels/<channel>/categories/<category>')
def show_channel_category(channel, category):
""" Shows TV Channel categories """
from resources.lib.modules.channels import Channels
Channels().show_channel_category(channel, category)
# @routing.route('/channels/<channel>/categories/<category>')
# def show_channel_category(channel, category):
# """ Shows TV Channel categories """
# from resources.lib.modules.channels import Channels
# Channels().show_channel_category(channel, category)
@routing.route('/channels/<channel>/tvguide')
@ -63,6 +63,13 @@ def show_channel_tvguide_detail(channel=None, date=None):
TvGuide().show_detail(channel, date)
@routing.route('/channels/<channel>/catalog')
def show_channel_catalog(channel):
""" Show the catalog of a channel """
from resources.lib.modules.catalog import Catalog
Catalog().show_catalog_channel(channel)
@routing.route('/catalog')
def show_catalog():
""" Show the catalog """
@ -70,32 +77,25 @@ def show_catalog():
Catalog().show_catalog()
@routing.route('/catalog/<channel>')
def show_channel_catalog(channel):
""" Show a category in the catalog """
from resources.lib.modules.catalog import Catalog
Catalog().show_catalog_channel(channel)
@routing.route('/catalog/<channel>/<program>')
def show_catalog_program(channel, program):
@routing.route('/catalog/<program>')
def show_catalog_program(program):
""" Show a program from the catalog """
from resources.lib.modules.catalog import Catalog
Catalog().show_program(channel, program)
Catalog().show_program(program)
@routing.route('/catalog/<channel>/<program>/clips')
def show_catalog_program_clips(channel, program):
@routing.route('/catalog/<program>/clips')
def show_catalog_program_clips(program):
""" Show the clips from a program """
from resources.lib.modules.catalog import Catalog
Catalog().show_program_clips(channel, program)
Catalog().show_program_clips(program)
@routing.route('/catalog/<channel>/<program>/season/<season>')
def show_catalog_program_season(channel, program, season):
@routing.route('/catalog/<program>/season/<season>')
def show_catalog_program_season(program, season):
""" Show a season from a program """
from resources.lib.modules.catalog import Catalog
Catalog().show_program_season(channel, program, season)
Catalog().show_program_season(program, season)
@routing.route('/search')
@ -127,8 +127,8 @@ def play_catalog(uuid):
Player().play(uuid)
@routing.route('/play/page/<channel>/<page>')
def play_from_page(channel, page):
@routing.route('/play/page/<page>')
def play_from_page(page):
""" Play the requested item """
try: # Python 3
from urllib.parse import unquote
@ -136,7 +136,7 @@ def play_from_page(channel, page):
from urllib import unquote
from resources.lib.modules.player import Player
Player().play_from_page(channel, unquote(page))
Player().play_from_page(unquote(page))
@routing.route('/metadata/update')

View File

@ -8,7 +8,6 @@ import logging
from resources.lib import kodiutils
from resources.lib.kodiutils import TitleItem
from resources.lib.modules.menu import Menu
from resources.lib.viervijfzes import CHANNELS
from resources.lib.viervijfzes.auth import AuthApi
from resources.lib.viervijfzes.content import CACHE_PREVENT, ContentApi, UnavailableException
@ -26,9 +25,7 @@ class Catalog:
def show_catalog(self):
""" Show all the programs of all channels """
try:
items = []
for channel in list(CHANNELS):
items.extend(self._api.get_programs(channel))
items = self._api.get_programs()
except Exception as ex:
kodiutils.notification(message=str(ex))
raise
@ -57,13 +54,12 @@ class Catalog:
# Used for A-Z listing or when movies and episodes are mixed.
kodiutils.show_listing(listing, 30003, content='tvshows', sort='title')
def show_program(self, channel, program_id):
def show_program(self, program_id):
""" Show a program from the catalog
:type channel: str
:type program_id: str
"""
try:
program = self._api.get_program(channel, program_id, extract_clips=True, cache=CACHE_PREVENT) # Use CACHE_PREVENT since we want fresh data
program = self._api.get_program(program_id, extract_clips=True, cache=CACHE_PREVENT) # Use CACHE_PREVENT since we want fresh data
except UnavailableException:
kodiutils.ok_dialog(message=kodiutils.localize(30717)) # This program is not available in the catalogue.
kodiutils.end_of_directory()
@ -76,11 +72,9 @@ class Catalog:
# Go directly to the season when we have only one season and no clips
if not program.clips and len(program.seasons) == 1:
self.show_program_season(channel, program_id, list(program.seasons.values())[0].uuid)
self.show_program_season(program_id, list(program.seasons.values())[0].uuid)
return
studio = CHANNELS.get(program.channel, {}).get('studio_icon')
listing = []
# Add an '* All seasons' entry when configured in Kodi
@ -88,7 +82,7 @@ class Catalog:
listing.append(
TitleItem(
title='* %s' % kodiutils.localize(30204), # * All seasons
path=kodiutils.url_for('show_catalog_program_season', channel=channel, program=program_id, season='-1'),
path=kodiutils.url_for('show_catalog_program_season', program=program_id, season='-1'),
art_dict={
'fanart': program.background,
},
@ -97,7 +91,6 @@ class Catalog:
'title': kodiutils.localize(30204), # All seasons
'plot': program.description,
'set': program.title,
'studio': studio,
}
)
)
@ -107,7 +100,7 @@ class Catalog:
listing.append(
TitleItem(
title=season.title, # kodiutils.localize(30205, season=season.number), # Season {season}
path=kodiutils.url_for('show_catalog_program_season', channel=channel, program=program_id, season=season.uuid),
path=kodiutils.url_for('show_catalog_program_season', program=program_id, season=season.uuid),
art_dict={
'fanart': program.background,
},
@ -116,7 +109,6 @@ class Catalog:
'title': kodiutils.localize(30205, season=season.number), # Season {season}
'plot': season.description,
'set': program.title,
'studio': studio,
}
)
)
@ -126,7 +118,7 @@ class Catalog:
listing.append(
TitleItem(
title=kodiutils.localize(30059, program=program.title), # Clips for {program}
path=kodiutils.url_for('show_catalog_program_clips', channel=channel, program=program_id),
path=kodiutils.url_for('show_catalog_program_clips', program=program_id),
art_dict={
'fanart': program.background,
},
@ -135,7 +127,6 @@ class Catalog:
'title': kodiutils.localize(30059, program=program.title), # Clips for {program}
'plot': kodiutils.localize(30060, program=program.title), # Watch short clips of {program}
'set': program.title,
'studio': studio,
}
)
)
@ -143,14 +134,13 @@ class Catalog:
# Sort by label. Some programs return seasons unordered.
kodiutils.show_listing(listing, 30003, content='tvshows')
def show_program_season(self, channel, program_id, season_uuid):
def show_program_season(self, program_id, season_uuid):
""" Show the episodes of a program from the catalog
:type channel: str
:type program_id: str
:type season_uuid: str
"""
try:
program = self._api.get_program(channel, program_id)
program = self._api.get_program(program_id)
except UnavailableException:
kodiutils.ok_dialog(message=kodiutils.localize(30717)) # This program is not available in the catalogue.
kodiutils.end_of_directory()
@ -168,14 +158,13 @@ class Catalog:
# Sort by episode number by default. Takes seasons into account.
kodiutils.show_listing(listing, 30003, content='episodes', sort=['episode', 'duration'])
def show_program_clips(self, channel, program_id):
def show_program_clips(self, program_id):
""" Show the clips of a program from the catalog
:type channel: str
:type program_id: str
"""
try:
# We need to query the backend, since we don't cache clips.
program = self._api.get_program(channel, program_id, extract_clips=True, cache=CACHE_PREVENT)
program = self._api.get_program(program_id, extract_clips=True, cache=CACHE_PREVENT)
except UnavailableException:
kodiutils.ok_dialog(message=kodiutils.localize(30717)) # This program is not available in the catalogue.
kodiutils.end_of_directory()

View File

@ -7,10 +7,9 @@ import logging
from resources.lib import kodiutils
from resources.lib.kodiutils import TitleItem
from resources.lib.modules.menu import Menu
from resources.lib.viervijfzes import CHANNELS, STREAM_DICT
from resources.lib.viervijfzes.auth import AuthApi
from resources.lib.viervijfzes.content import CACHE_AUTO, CACHE_ONLY, ContentApi
from resources.lib.viervijfzes.content import ContentApi
_LOGGER = logging.getLogger(__name__)
@ -55,7 +54,6 @@ class Channels:
'plot': None,
'playcount': 0,
'mediatype': 'video',
'studio': channel.get('studio_icon'),
},
stream_dict=STREAM_DICT,
context_menu=context_menu
@ -74,18 +72,24 @@ 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'))
listing = [
TitleItem(
title=kodiutils.localize(30053, channel=channel_info.get('name')), # TV Guide for {channel}
path=kodiutils.url_for('show_channel_tvguide', channel=channel),
art_dict={
'icon': 'DefaultAddonTvInfo.png',
'fanart': fanart,
},
info_dict={
'plot': kodiutils.localize(30054, channel=channel_info.get('name')), # Browse the TV Guide for {channel}
}
),
listing = []
if channel_info.get('epg_id'):
listing.append(
TitleItem(
title=kodiutils.localize(30053, channel=channel_info.get('name')), # TV Guide for {channel}
path=kodiutils.url_for('show_channel_tvguide', channel=channel),
art_dict={
'icon': 'DefaultAddonTvInfo.png',
'fanart': fanart,
},
info_dict={
'plot': kodiutils.localize(30054, channel=channel_info.get('name')), # Browse the TV Guide for {channel}
}
)
)
listing.append(
TitleItem(
title=kodiutils.localize(30055, channel=channel_info.get('name')), # Catalog for {channel}
path=kodiutils.url_for('show_channel_catalog', channel=channel),
@ -96,19 +100,22 @@ class Channels:
info_dict={
'plot': kodiutils.localize(30056, channel=channel_info.get('name')), # Browse the Catalog for {channel}
}
),
TitleItem(
title=kodiutils.localize(30057, channel=channel_info.get('name')), # Categories for {channel}
path=kodiutils.url_for('show_channel_categories', channel=channel),
art_dict={
'icon': 'DefaultGenre.png',
'fanart': fanart,
},
info_dict={
'plot': kodiutils.localize(30058, channel=channel_info.get('name')), # Browse the Categories for {channel}
}
),
]
)
)
# listing.append(
# TitleItem(
# title=kodiutils.localize(30057, channel=channel_info.get('name')), # Categories for {channel}
# path=kodiutils.url_for('show_channel_categories', channel=channel),
# art_dict={
# 'icon': 'DefaultGenre.png',
# 'fanart': fanart,
# },
# info_dict={
# 'plot': kodiutils.localize(30058, channel=channel_info.get('name')), # Browse the Categories for {channel}
# }
# )
# )
# Add YouTube channels
if kodiutils.get_cond_visibility('System.HasAddon(plugin.video.youtube)') != 0:
@ -125,57 +132,57 @@ class Channels:
kodiutils.show_listing(listing, 30007, sort=['unsorted'])
def show_channel_categories(self, channel):
""" Shows the categories of a channel
:type channel: str
"""
categories = self._api.get_categories(channel)
# def show_channel_categories(self, channel):
# """ Shows the categories of a channel
# :type channel: str
# """
# categories = self._api.get_categories(channel)
#
# listing = [
# TitleItem(
# title=category.title,
# path=kodiutils.url_for('show_channel_category', channel=category.channel, category=category.uuid),
# art_dict={
# 'icon': 'DefaultGenre.png',
# },
# ) for category in categories
# ]
#
# kodiutils.show_listing(listing, 30007, sort=['unsorted'])
listing = [
TitleItem(
title=category.title,
path=kodiutils.url_for('show_channel_category', channel=category.channel, category=category.uuid),
art_dict={
'icon': 'DefaultGenre.png',
},
) for category in categories
]
kodiutils.show_listing(listing, 30007, sort=['unsorted'])
def show_channel_category(self, channel, category_id):
""" Shows a selected category of a channel
:type channel: str
:type category_id: str
"""
categories = self._api.get_categories(channel)
# Extract selected category
category = next(category for category in categories if category.uuid == category_id)
if not category:
raise Exception('Unknown category')
# Add programs
listing_programs = []
for item in category.programs:
program = self._api.get_program(channel, item.path, CACHE_ONLY) # Get program details, but from cache only
if program:
listing_programs.append(Menu.generate_titleitem(program))
else:
listing_programs.append(Menu.generate_titleitem(item))
# Add episodes
listing_episodes = []
for item in category.episodes:
# We don't have the Program Name without making a request to the page, so we use CACHE_AUTO instead of CACHE_ONLY.
# This will make a request for each item in this view (about 12 items), but it goes quite fast.
# Results are cached, so this will only happen once.
episode = self._api.get_episode(channel, item.path, CACHE_AUTO)
if episode:
listing_episodes.append(Menu.generate_titleitem(episode))
else:
listing_episodes.append(Menu.generate_titleitem(item))
kodiutils.show_listing(listing_programs + listing_episodes, 30007, content='tvshows', sort=['unsorted'])
# def show_channel_category(self, channel, category_id):
# """ Shows a selected category of a channel
# :type channel: str
# :type category_id: str
# """
# categories = self._api.get_categories(channel)
#
# # Extract selected category
# category = next(category for category in categories if category.uuid == category_id)
# if not category:
# raise Exception('Unknown category')
#
# # Add programs
# listing_programs = []
# for item in category.programs:
# program = self._api.get_program(item.path, CACHE_ONLY) # Get program details, but from cache only
#
# if program:
# listing_programs.append(Menu.generate_titleitem(program))
# else:
# listing_programs.append(Menu.generate_titleitem(item))
#
# # Add episodes
# listing_episodes = []
# for item in category.episodes:
# # We don't have the Program Name without making a request to the page, so we use CACHE_AUTO instead of CACHE_ONLY.
# # This will make a request for each item in this view (about 12 items), but it goes quite fast.
# # Results are cached, so this will only happen once.
# episode = self._api.get_episode(item.path, CACHE_AUTO)
#
# if episode:
# listing_episodes.append(Menu.generate_titleitem(episode))
# else:
# listing_episodes.append(Menu.generate_titleitem(item))
#
# kodiutils.show_listing(listing_programs + listing_episodes, 30007, content='tvshows', sort=['unsorted'])

View File

@ -41,17 +41,16 @@ class IPTVManager:
"""Return JSON-STREAMS formatted information to IPTV Manager"""
streams = []
for key, channel in CHANNELS.items():
item = dict(
id=channel.get('iptv_id'),
name=channel.get('name'),
logo='special://home/addons/{addon}/resources/logos/{logo}'.format(addon=kodiutils.addon_id(),
logo=channel.get('logo')),
preset=channel.get('iptv_preset'),
stream='plugin://plugin.video.viervijfzes/play/live/{channel}'.format(channel=key),
vod='plugin://plugin.video.viervijfzes/play/epg/{channel}/{{date}}'.format(channel=key)
)
streams.append(item)
if channel.get('iptv_id'):
streams.append(dict(
id=channel.get('iptv_id'),
name=channel.get('name'),
logo='special://home/addons/{addon}/resources/logos/{logo}'.format(addon=kodiutils.addon_id(),
logo=channel.get('logo')),
preset=channel.get('iptv_preset'),
stream='plugin://plugin.video.viervijfzes/play/live/{channel}'.format(channel=key),
vod='plugin://plugin.video.viervijfzes/play/epg/{channel}/{{date}}'.format(channel=key)
))
return dict(version=1, streams=streams)
@ -69,25 +68,26 @@ class IPTVManager:
for key, channel in CHANNELS.items():
iptv_id = channel.get('iptv_id')
results[iptv_id] = []
for date in ['yesterday', 'today', 'tomorrow']:
epg = epg_api.get_epg(key, date)
if channel.get('iptv_id'):
results[iptv_id] = []
for date in ['yesterday', 'today', 'tomorrow']:
epg = epg_api.get_epg(key, date)
results[iptv_id].extend([
dict(
start=program.start.isoformat(),
stop=(program.start + timedelta(seconds=program.duration)).isoformat(),
title=program.program_title,
subtitle=program.episode_title,
description=program.description,
episode='S%sE%s' % (program.season, program.number) if program.season and program.number else None,
genre=program.genre,
genre_id=program.genre_id,
image=program.cover,
stream=kodiutils.url_for('play_from_page',
channel=key,
page=quote(program.video_url, safe='')) if program.video_url else None)
for program in epg if program.duration
])
results[iptv_id].extend([
dict(
start=program.start.isoformat(),
stop=(program.start + timedelta(seconds=program.duration)).isoformat(),
title=program.program_title,
subtitle=program.episode_title,
description=program.description,
episode='S%sE%s' % (program.season, program.number) if program.season and program.number else None,
genre=program.genre,
genre_id=program.genre_id,
image=program.cover,
stream=kodiutils.url_for('play_from_page',
channel=key,
page=quote(program.video_url, safe='')) if program.video_url else None)
for program in epg if program.duration
])
return dict(version=1, epg=results)

View File

@ -5,7 +5,7 @@ from __future__ import absolute_import, division, unicode_literals
from resources.lib import kodiutils
from resources.lib.kodiutils import TitleItem
from resources.lib.viervijfzes import CHANNELS, STREAM_DICT
from resources.lib.viervijfzes import STREAM_DICT
from resources.lib.viervijfzes.content import Episode, Program
@ -70,7 +70,6 @@ class Menu:
info_dict = {
'title': item.title,
'plot': item.description,
'studio': CHANNELS.get(item.channel, {}).get('studio_icon'),
'aired': item.aired.strftime('%Y-%m-%d') if item.aired else None,
}
prop_dict = {}

View File

@ -35,13 +35,12 @@ class Player:
kodiutils.ok_dialog(message=kodiutils.localize(30718, channel=channel_name.get('name'))) # There is no live stream available for {channel}.
kodiutils.end_of_directory()
def play_from_page(self, channel, path):
def play_from_page(self, path):
""" Play the requested item.
:type channel: string
:type path: string
"""
# Get episode information
episode = self._api.get_episode(channel, path)
episode = self._api.get_episode(path)
resolved_stream = None
if episode is None:

View File

@ -181,4 +181,4 @@ class TvGuide:
kodiutils.end_of_directory()
return
Player().play_from_page(channel, broadcast.video_url)
Player().play_from_page(broadcast.video_url)

View File

@ -1,50 +1,70 @@
# -*- coding: utf-8 -*-
""" SBS API """
""" GoPlay API """
from __future__ import absolute_import, division, unicode_literals
from collections import OrderedDict
CHANNELS = OrderedDict([
('vier', dict(
name='VIER',
url='https://www.vier.be',
logo='vier.png',
background='vier-background.jpg',
studio_icon='vier',
('Play4', dict(
name='Play4',
epg_id='vier',
logo='play4.png',
background='play4-background.png',
iptv_preset=4,
iptv_id='vier.be',
iptv_id='play4.be',
youtube=[
dict(
label='VIER / VIJF',
logo='vier.png',
label='GoPlay',
logo='goplay.png',
path='plugin://plugin.video.youtube/user/viertv/',
),
],
)),
('vijf', dict(
name='VIJF',
url='https://www.vijf.be',
logo='vijf.png',
background='vijf-background.jpg',
studio_icon='vijf',
('Play5', dict(
name='Play5',
epg_id='vijf',
logo='play5.png',
background='play5-background.png',
iptv_preset=5,
iptv_id='vijf.be',
iptv_id='play5.be',
youtube=[
dict(
label='VIER / VIJF',
logo='vijf.png',
label='GoPlay',
logo='goplay.png',
path='plugin://plugin.video.youtube/user/viertv/',
),
],
)),
('zes', dict(
name='ZES',
url='https://www.zestv.be',
logo='zes.png',
background='zes-background.jpg',
studio_icon='zes',
('Play6', dict(
name='Play6',
epg_id='zes',
logo='play6.png',
background='play6-background.png',
iptv_preset=6,
iptv_id='zes.be',
iptv_id='play6.be',
youtube=[
dict(
label='GoPlay',
logo='goplay.png',
path='plugin://plugin.video.youtube/user/viertv/',
),
],
)),
# ('Play7', dict(
# name='Play7',
# epg_id='zeven',
# url='https://www.goplay.be',
# logo='play7.png',
# background='play7-background.png',
# iptv_preset=7,
# iptv_id='play7.be',
# youtube=[],
# )),
('GoPlay', dict(
name='Go Play',
url='https://www.goplay.be',
logo='goplay.png',
background='goplay-background.png',
youtube=[],
))
])

View File

@ -15,7 +15,7 @@ _LOGGER = logging.getLogger(__name__)
class AuthApi:
""" VIER/VIJF/ZES Authentication API """
""" GoPlay Authentication API """
COGNITO_REGION = 'eu-west-1'
COGNITO_POOL_ID = 'eu-west-1_dViSsKM5Y'
COGNITO_CLIENT_ID = '6s1h851s8uplco5h6mqh1jac8m'

View File

@ -13,7 +13,7 @@ from datetime import datetime
import requests
from resources.lib.kodiutils import STREAM_DASH, STREAM_HLS
from resources.lib.viervijfzes import CHANNELS, ResolvedStream
from resources.lib.viervijfzes import ResolvedStream
try: # Python 3
from html import unescape
@ -169,14 +169,10 @@ class Category:
class ContentApi:
""" VIER/VIJF/ZES Content API"""
API_ENDPOINT = 'https://api.viervijfzes.be'
API2_ENDPOINT = 'https://api2.viervijfzes.be'
SITE_APIS = {
'vier': 'https://www.vier.be/api',
'vijf': 'https://www.vijf.be/api',
'zes': 'https://www.zestv.be/api',
}
""" GoPlay Content API"""
SITE_URL = 'https://www.goplay.be'
API_VIERVIJFZES = 'https://api.viervijfzes.be'
API_GOPLAY = 'https://api.goplay.be'
def __init__(self, auth=None, cache_path=None):
""" Initialise object """
@ -184,64 +180,53 @@ class ContentApi:
self._auth = auth
self._cache_path = cache_path
def get_programs(self, channel, cache=CACHE_AUTO):
def get_programs(self, channel=None, cache=CACHE_AUTO):
""" Get a list of all programs of the specified channel.
:type channel: str
:type cache: str
:rtype list[Program]
"""
if channel not in CHANNELS:
raise Exception('Unknown channel %s' % channel)
def update():
""" Fetch the program listing by scraping """
# Load webpage
raw_html = self._get_url(CHANNELS[channel]['url'])
raw_html = self._get_url(self.SITE_URL + '/programmas')
# Parse programs
regex_programs = re.compile(r'<a class="program-overview__link" href="(?P<path>[^"]+)">\s+'
r'<span class="program-overview__title">\s+(?P<title>[^<]+)</span>.*?'
r'</a>', re.DOTALL)
data = {
item.group('path').lstrip('/'): unescape(item.group('title').strip())
regex_programs = re.compile(r'data-program="(?P<json>[^"]+)"', re.DOTALL)
data = [
json.loads(unescape(item.group('json')))
for item in regex_programs.finditer(raw_html)
}
]
if not data:
raise Exception('No programs found for %s' % channel)
raise Exception('No programs found')
return data
# Fetch listing from cache or update if needed
data = self._handle_cache(key=['programs', channel], cache_mode=cache, update=update, ttl=30 * 5)
data = self._handle_cache(key=['programs'], cache_mode=cache, update=update, ttl=30 * 5)
if not data:
return []
programs = []
for path in data:
title = data[path]
program = self.get_program(channel, path, cache=CACHE_ONLY) # Get program details, but from cache only
if program:
# Use program with metadata from cache
programs.append(program)
else:
# Use program with the values that we've parsed from the page
programs.append(Program(channel=channel,
path=path,
title=title))
if channel:
programs = [
self._parse_program_data(record) for record in data if record['pageInfo']['brand'] == channel
]
else:
programs = [
self._parse_program_data(record) for record in data
]
return programs
def get_program(self, channel, path, extract_clips=False, cache=CACHE_AUTO):
def get_program(self, path, extract_clips=False, cache=CACHE_AUTO):
""" Get a Program object from the specified page.
:type channel: str
:type path: str
:type extract_clips: bool
:type cache: int
:rtype Program
"""
if channel not in CHANNELS:
raise Exception('Unknown channel %s' % channel)
# We want to use the html to extract clips
# This is the worst hack, since Python 2.7 doesn't support nonlocal
raw_html = [None]
@ -249,7 +234,7 @@ class ContentApi:
def update():
""" Fetch the program metadata by scraping """
# Fetch webpage
page = self._get_url(CHANNELS[channel]['url'] + '/' + path)
page = self._get_url(self.SITE_URL + '/' + path)
# Store a copy in the parent's raw_html var.
raw_html[0] = page
@ -262,7 +247,7 @@ class ContentApi:
return data
# Fetch listing from cache or update if needed
data = self._handle_cache(key=['program', channel, path], cache_mode=cache, update=update)
data = self._handle_cache(key=['program', path], cache_mode=cache, update=update)
if not data:
return None
@ -270,25 +255,22 @@ class ContentApi:
# Also extract clips if we did a real HTTP call
if extract_clips and raw_html[0]:
clips = self._extract_videos(raw_html[0], channel)
clips = self._extract_videos(raw_html[0])
program.clips = clips
return program
def get_episode(self, channel, path, cache=CACHE_AUTO):
def get_episode(self, path, cache=CACHE_AUTO):
""" Get a Episode object from the specified page.
:type channel: str
:type path: str
:type cache: str
:rtype Episode
"""
if channel not in CHANNELS:
raise Exception('Unknown channel %s' % channel)
def update():
""" Fetch the program metadata by scraping """
# Load webpage
page = self._get_url(CHANNELS[channel]['url'] + '/' + path)
page = self._get_url(self.SITE_URL + '/' + path)
program_json = None
episode_json = None
@ -299,7 +281,7 @@ class ContentApi:
result = regex_video_data.search(page)
if result:
video_id = json.loads(unescape(result.group(1)))['id']
video_json_data = self._get_url('%s/video/%s' % (self.SITE_APIS[channel], video_id))
video_json_data = self._get_url('%s/api/video/%s' % (self.SITE_URL, video_id))
video_json = json.loads(video_json_data)
return dict(video=video_json)
@ -320,7 +302,7 @@ class ContentApi:
return dict(program=program_json, episode=episode_json)
# Fetch listing from cache or update if needed
data = self._handle_cache(key=['episode', channel, path], cache_mode=cache, update=update)
data = self._handle_cache(key=['episode', path], cache_mode=cache, update=update)
if not data:
return None
@ -344,7 +326,7 @@ class ContentApi:
:type uuid: str
:rtype str
"""
response = self._get_url(self.API_ENDPOINT + '/content/%s' % uuid, authentication=True)
response = self._get_url(self.API_VIERVIJFZES + '/content/%s' % uuid, authentication=True)
data = json.loads(response)
if 'videoDash' in data:
@ -353,7 +335,7 @@ class ContentApi:
drm_key = data['drmKey']['S']
_LOGGER.debug('Fetching Authentication XML with drm_key %s', drm_key)
response_drm = self._get_url(self.API2_ENDPOINT + '/decode/%s' % drm_key, authentication=True)
response_drm = self._get_url(self.API_GOPLAY + '/restricted/decode/%s' % drm_key, authentication=True)
data_drm = json.loads(response_drm)
return ResolvedStream(
@ -371,68 +353,64 @@ class ContentApi:
stream_type=STREAM_HLS,
)
def get_categories(self, channel):
""" Get a list of all categories of the specified channel.
:type channel: str
:rtype list[Category]
"""
if channel not in CHANNELS:
raise Exception('Unknown channel %s' % channel)
# def get_categories(self):
# """ Get a list of all categories.
# :rtype list[Category]
# """
# # Load webpage
# raw_html = self._get_url(self.SITE_URL)
#
# # Categories regexes
# regex_articles = re.compile(r'<article([^>]+)>(.*?)</article>', re.DOTALL)
# regex_submenu_id = re.compile(r'data-submenu-id="([^"]*)"') # splitted since the order might change
# regex_submenu_title = re.compile(r'data-submenu-title="([^"]*)"')
#
# categories = []
# for result in regex_articles.finditer(raw_html):
# article_info_html = result.group(1)
# article_html = result.group(2)
# category_title = regex_submenu_title.search(article_info_html).group(1)
# category_id = regex_submenu_id.search(article_info_html).group(1)
#
# # Skip empty categories or 'All programs'
# if not category_id or category_id == 'programmas':
# continue
#
# # Extract items
# programs = self._extract_programs(article_html, channel)
# episodes = self._extract_videos(article_html)
# categories.append(Category(uuid=category_id, channel=channel, title=category_title, programs=programs, episodes=episodes))
#
# return categories
# Load webpage
raw_html = self._get_url(CHANNELS[channel]['url'])
# Categories regexes
regex_articles = re.compile(r'<article([^>]+)>(.*?)</article>', re.DOTALL)
regex_submenu_id = re.compile(r'data-submenu-id="([^"]*)"') # splitted since the order might change
regex_submenu_title = re.compile(r'data-submenu-title="([^"]*)"')
categories = []
for result in regex_articles.finditer(raw_html):
article_info_html = result.group(1)
article_html = result.group(2)
category_title = regex_submenu_title.search(article_info_html).group(1)
category_id = regex_submenu_id.search(article_info_html).group(1)
# Skip empty categories or 'All programs'
if not category_id or category_id == 'programmas':
continue
# Extract items
programs = self._extract_programs(article_html, channel)
episodes = self._extract_videos(article_html, channel)
categories.append(Category(uuid=category_id, channel=channel, title=category_title, programs=programs, episodes=episodes))
return categories
# @staticmethod
# def _extract_programs(html, channel):
# """ Extract Programs from HTML code """
# # Item regexes
# regex_item = re.compile(r'<a[^>]+?href="(?P<path>[^"]+)"[^>]+?>'
# r'.*?<h3 class="poster-teaser__title"><span>(?P<title>[^<]*)</span></h3>.*?'
# r'</a>', re.DOTALL)
#
# # Extract items
# programs = []
# for item in regex_item.finditer(html):
# path = item.group('path')
# if path.startswith('/video'):
# continue
#
# title = unescape(item.group('title'))
#
# # Program
# programs.append(Program(
# path=path.lstrip('/'),
# channel=channel,
# title=title,
# ))
#
# return programs
@staticmethod
def _extract_programs(html, channel):
""" Extract Programs from HTML code """
# Item regexes
regex_item = re.compile(r'<a[^>]+?href="(?P<path>[^"]+)"[^>]+?>'
r'.*?<h3 class="poster-teaser__title"><span>(?P<title>[^<]*)</span></h3>.*?'
r'</a>', re.DOTALL)
# Extract items
programs = []
for item in regex_item.finditer(html):
path = item.group('path')
if path.startswith('/video'):
continue
title = unescape(item.group('title'))
# Program
programs.append(Program(
path=path.lstrip('/'),
channel=channel,
title=title,
))
return programs
@staticmethod
def _extract_videos(html, channel):
def _extract_videos(html):
""" Extract videos from HTML code """
# Item regexes
regex_item = re.compile(r'<a[^>]+?href="(?P<path>[^"]+)"[^>]+?>.*?</a>', re.DOTALL)
@ -490,7 +468,7 @@ class ContentApi:
# Episode
episodes.append(Episode(
path=path.lstrip('/'),
channel=channel,
channel='', # TODO
title=title,
duration=episode_duration,
uuid=episode_video_id,
@ -511,7 +489,7 @@ class ContentApi:
program = Program(
uuid=data['id'],
path=data['link'].lstrip('/'),
channel=data['pageInfo']['site'],
channel=data['pageInfo']['brand'],
title=data['title'],
description=data['description'],
aired=datetime.fromtimestamp(data.get('pageInfo', {}).get('publishDate')),
@ -524,7 +502,7 @@ class ContentApi:
key: Season(
uuid=playlist['id'],
path=playlist['link'].lstrip('/'),
channel=playlist['pageInfo']['site'],
channel=playlist['pageInfo']['brand'],
title=playlist['title'],
description=playlist['pageInfo']['description'],
number=playlist['episodes'][0]['seasonNumber'], # You did not see this

View File

@ -67,12 +67,12 @@ class EpgProgram:
class EpgApi:
""" VIER/VIJF/ZES EPG API """
""" GoPlay EPG API """
EPG_ENDPOINTS = {
'vier': 'https://www.vier.be/api/epg/{date}',
'vijf': 'https://www.vijf.be/api/epg/{date}',
'zes': 'https://www.zestv.be/api/epg/{date}',
'Play4': 'https://www.goplay.be/api/epg/vier/{date}',
'Play5': 'https://www.goplay.be/api/epg/vijf/{date}',
'Play6': 'https://www.goplay.be/api/epg/zes/{date}'
}
EPG_NO_BROADCAST = 'Geen uitzending'

View File

@ -14,7 +14,7 @@ _LOGGER = logging.getLogger(__name__)
class SearchApi:
""" VIER/VIJF/ZES Search API """
""" GoPlay Search API """
API_ENDPOINT = 'https://api.viervijfzes.be/search'
def __init__(self):

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
resources/logos/goplay.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
resources/logos/play4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
resources/logos/play5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
resources/logos/play6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

View File

@ -11,7 +11,7 @@ import unittest
import resources.lib.kodiutils as kodiutils
from resources.lib.viervijfzes import ResolvedStream
from resources.lib.viervijfzes.auth import AuthApi
from resources.lib.viervijfzes.content import ContentApi, Program, Episode, Category, CACHE_PREVENT
from resources.lib.viervijfzes.content import ContentApi, Program, Episode, CACHE_PREVENT
_LOGGER = logging.getLogger(__name__)
@ -23,39 +23,36 @@ class TestApi(unittest.TestCase):
self._api = ContentApi(auth, cache_path=kodiutils.get_cache_path())
def test_programs(self):
for channel in ['vier', 'vijf', 'zes']:
programs = self._api.get_programs(channel)
self.assertIsInstance(programs, list)
self.assertIsInstance(programs[0], Program)
programs = self._api.get_programs()
self.assertIsInstance(programs, list)
self.assertIsInstance(programs[0], Program)
def test_categories(self):
for channel in ['vier', 'vijf', 'zes']:
categories = self._api.get_categories(channel)
self.assertIsInstance(categories, list)
if categories:
self.assertIsInstance(categories[0], Category)
# def test_categories(self):
# categories = self._api.get_categories()
# self.assertIsInstance(categories, list)
# self.assertIsInstance(categories[0], Category)
def test_episodes(self):
for channel, program in [('vier', 'auwch'), ('vijf', 'zo-man-zo-vrouw')]:
program = self._api.get_program(channel, program, cache=CACHE_PREVENT)
for program in ['auwch', 'zo-man-zo-vrouw']:
program = self._api.get_program(program, cache=CACHE_PREVENT)
self.assertIsInstance(program, Program)
self.assertIsInstance(program.seasons, dict)
self.assertIsInstance(program.episodes, list)
self.assertIsInstance(program.episodes[0], Episode)
def test_clips(self):
for channel, program in [('vier', 'gert-late-night')]:
program = self._api.get_program(channel, program, extract_clips=True, cache=CACHE_PREVENT)
for program in ['gert-late-night']:
program = self._api.get_program(program, extract_clips=True, cache=CACHE_PREVENT)
self.assertIsInstance(program.clips, list)
self.assertIsInstance(program.clips[0], Episode)
episode = self._api.get_episode(channel, program.clips[0].path, cache=CACHE_PREVENT)
episode = self._api.get_episode(program.clips[0].path, cache=CACHE_PREVENT)
self.assertIsInstance(episode, Episode)
@unittest.skipUnless(kodiutils.get_setting('username') and kodiutils.get_setting('password'), 'Skipping since we have no credentials.')
def test_get_stream(self):
program = self._api.get_program('vier', 'auwch')
program = self._api.get_program('auwch')
self.assertIsInstance(program, Program)
episode = program.episodes[0]
@ -64,7 +61,7 @@ class TestApi(unittest.TestCase):
@unittest.skipUnless(kodiutils.get_setting('username') and kodiutils.get_setting('password'), 'Skipping since we have no credentials.')
def test_get_drm_stream(self):
resolved_stream = self._api.get_stream_by_uuid('b56a41d9-5a6a-4ff0-b16a-56dc7b2e1f25')
resolved_stream = self._api.get_stream_by_uuid('a545bb4f-0f4c-4588-92a2-5086041c0e08') # Hawaii Five-O 8x12
self.assertIsInstance(resolved_stream, ResolvedStream)

View File

@ -22,17 +22,17 @@ class TestEpg(unittest.TestCase):
self._epg = EpgApi()
def test_vier_today(self):
programs = self._epg.get_epg('vier', date.today().strftime('%Y-%m-%d'))
programs = self._epg.get_epg('Play4', date.today().strftime('%Y-%m-%d'))
self.assertIsInstance(programs, list)
self.assertIsInstance(programs[0], EpgProgram)
def test_vijf_today(self):
programs = self._epg.get_epg('vijf', date.today().strftime('%Y-%m-%d'))
programs = self._epg.get_epg('Play5', date.today().strftime('%Y-%m-%d'))
self.assertIsInstance(programs, list)
self.assertIsInstance(programs[0], EpgProgram)
def test_zes_today(self):
programs = self._epg.get_epg('zes', date.today().strftime('%Y-%m-%d'))
programs = self._epg.get_epg('Play6', date.today().strftime('%Y-%m-%d'))
self.assertIsInstance(programs, list)
self.assertIsInstance(programs[0], EpgProgram)
@ -41,16 +41,16 @@ class TestEpg(unittest.TestCase):
self._epg.get_epg('vtm', date.today().strftime('%Y-%m-%d'))
def test_vier_out_of_range(self):
programs = self._epg.get_epg('vier', '2020-01-01')
programs = self._epg.get_epg('Play4', '2020-01-01')
self.assertEqual(programs, [])
def test_play_video_from_epg(self):
epg_programs = self._epg.get_epg('vier', 'yesterday')
epg_programs = self._epg.get_epg('Play4', 'yesterday')
epg_program = [program for program in epg_programs if program.video_url][0]
# Lookup the Episode data since we don't have an UUID
api = ContentApi(cache_path=kodiutils.get_cache_path())
episode = api.get_episode(epg_program.channel, epg_program.video_url)
episode = api.get_episode(epg_program.video_url)
self.assertIsInstance(episode, Episode)
# def test_map_epg_genre(self):

View File

@ -29,27 +29,27 @@ class TestRouting(unittest.TestCase):
def test_channels_menu(self):
routing.run([routing.url_for(addon.show_channels), '0', ''])
routing.run([routing.url_for(addon.show_channel_menu, channel='vier'), '0', ''])
routing.run([routing.url_for(addon.show_channel_menu, channel='Play4'), '0', ''])
def test_catalog_menu(self):
routing.run([routing.url_for(addon.show_catalog), '0', ''])
def test_catalog_channel_menu(self):
routing.run([routing.url_for(addon.show_channel_catalog, channel='vier'), '0', ''])
routing.run([routing.url_for(addon.show_channel_catalog, channel='Play4'), '0', ''])
def test_catalog_program_menu(self):
routing.run([routing.url_for(addon.show_catalog_program, channel='vier', program='de-mol'), '0', ''])
routing.run([routing.url_for(addon.show_catalog_program, channel='Play4', program='de-mol'), '0', ''])
def test_catalog_program_season_menu(self):
routing.run([routing.url_for(addon.show_catalog_program_season, channel='vier', program='de-mol', season=-1), '0', ''])
routing.run([routing.url_for(addon.show_catalog_program_season, channel='Play4', program='de-mol', season=-1), '0', ''])
def test_search_menu(self):
routing.run([routing.url_for(addon.show_search), '0', ''])
routing.run([routing.url_for(addon.show_search, query='de mol'), '0', ''])
def test_tvguide_menu(self):
routing.run([routing.url_for(addon.show_channel_tvguide, channel='vier'), '0', ''])
routing.run([routing.url_for(addon.show_channel_tvguide_detail, channel='vier', date='today'), '0', ''])
routing.run([routing.url_for(addon.show_channel_tvguide, channel='Play4'), '0', ''])
routing.run([routing.url_for(addon.show_channel_tvguide_detail, channel='Play4', date='today'), '0', ''])
# def test_metadata_update(self):
# routing.run([routing.url_for(addon.metadata_update), '0', ''])