Add recommendations and categories (#76)

This commit is contained in:
Michaël Arnauts 2021-02-17 07:42:24 +01:00 committed by GitHub
parent 9ddc73094d
commit 88e1bbc4d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 360 additions and 194 deletions

View File

@ -18,6 +18,18 @@ msgctxt "#30003"
msgid "Catalogue" msgid "Catalogue"
msgstr "" msgstr ""
msgctxt "#30004"
msgid "Browse the catalogue"
msgstr ""
msgctxt "#30005"
msgid "Recommendations"
msgstr ""
msgctxt "#30006"
msgid "Show the recommendations"
msgstr ""
msgctxt "#30007" msgctxt "#30007"
msgid "Channels" msgid "Channels"
msgstr "" msgstr ""
@ -64,14 +76,6 @@ msgctxt "#30056"
msgid "Browse the Catalog for [B]{channel}[/B]" msgid "Browse the Catalog for [B]{channel}[/B]"
msgstr "" msgstr ""
msgctxt "#30057"
msgid "Categories for [B]{channel}[/B]"
msgstr ""
msgctxt "#30058"
msgid "Browse the Categories for [B]{channel}[/B]"
msgstr ""
msgctxt "#30059" msgctxt "#30059"
msgid "Clips of [B]{program}[/B]" msgid "Clips of [B]{program}[/B]"
msgstr "" msgstr ""
@ -186,6 +190,14 @@ msgctxt "#30803"
msgid "Password" msgid "Password"
msgstr "" msgstr ""
msgctxt "#30820"
msgid "Interface"
msgstr ""
msgctxt "#30821"
msgid "Show unavailable programs"
msgstr ""
msgctxt "#30840" msgctxt "#30840"
msgid "Integration" msgid "Integration"
msgstr "" msgstr ""

View File

@ -19,6 +19,18 @@ msgctxt "#30003"
msgid "Catalogue" msgid "Catalogue"
msgstr "Catalogus" msgstr "Catalogus"
msgctxt "#30004"
msgid "Browse the catalogue"
msgstr "Doorblader de catalogus"
msgctxt "#30005"
msgid "Recommendations"
msgstr "Aanbevelingen"
msgctxt "#30006"
msgid "Show the recommendations"
msgstr "Doorblader de aanbevelingen"
msgctxt "#30007" msgctxt "#30007"
msgid "Channels" msgid "Channels"
msgstr "Kanalen" msgstr "Kanalen"
@ -65,14 +77,6 @@ msgctxt "#30056"
msgid "Browse the Catalog for [B]{channel}[/B]" msgid "Browse the Catalog for [B]{channel}[/B]"
msgstr "Doorblader de catalogus voor [B]{channel}[/B]" msgstr "Doorblader de catalogus voor [B]{channel}[/B]"
msgctxt "#30057"
msgid "Categories for [B]{channel}[/B]"
msgstr "Categoriën voor [B]{channel}[/B]"
msgctxt "#30058"
msgid "Browse the Categories for [B]{channel}[/B]"
msgstr "Doorblader de categoriën van [B]{channel}[/B]"
msgctxt "#30059" msgctxt "#30059"
msgid "Clips of [B]{program}[/B]" msgid "Clips of [B]{program}[/B]"
msgstr "Clips van [B]{program}[/B]" msgstr "Clips van [B]{program}[/B]"
@ -187,6 +191,14 @@ msgctxt "#30803"
msgid "Password" msgid "Password"
msgstr "Wachtwoord" msgstr "Wachtwoord"
msgctxt "#30820"
msgid "Interface"
msgstr "Interface"
msgctxt "#30821"
msgid "Show unavailable programs"
msgstr "Toon onbeschikbare programma's"
msgctxt "#30840" msgctxt "#30840"
msgid "Integration" msgid "Integration"
msgstr "Integratie" msgstr "Integratie"

View File

@ -9,6 +9,11 @@ from routing import Plugin
from resources.lib import kodilogging from resources.lib import kodilogging
try: # Python 3
from urllib.parse import unquote
except ImportError: # Python 2
from urllib import unquote
routing = Plugin() # pylint: disable=invalid-name routing = Plugin() # pylint: disable=invalid-name
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -34,20 +39,6 @@ def show_channel_menu(channel):
Channels().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/<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') @routing.route('/channels/<channel>/tvguide')
def show_channel_tvguide(channel): def show_channel_tvguide(channel):
""" Shows the dates in the tv guide """ """ Shows the dates in the tv guide """
@ -97,6 +88,34 @@ def show_catalog_program_season(program, season):
Catalog().show_program_season(program, season) Catalog().show_program_season(program, season)
@routing.route('/category')
def show_categories():
""" Show the catalog by category """
from resources.lib.modules.catalog import Catalog
Catalog().show_categories()
@routing.route('/category/<category>')
def show_category(category):
""" Show the catalog by category """
from resources.lib.modules.catalog import Catalog
Catalog().show_category(category)
@routing.route('/recommendations')
def show_recommendations():
""" Show my list """
from resources.lib.modules.catalog import Catalog
Catalog().show_recommendations()
@routing.route('/recommendations/<category>')
def show_recommendations_category(category):
""" Show my list """
from resources.lib.modules.catalog import Catalog
Catalog().show_recommendations_category(category)
@routing.route('/mylist') @routing.route('/mylist')
def show_mylist(): def show_mylist():
""" Show my list """ """ Show my list """
@ -150,11 +169,6 @@ def play_catalog(uuid):
@routing.route('/play/page/<page>') @routing.route('/play/page/<page>')
def play_from_page(page): def play_from_page(page):
""" Play the requested item """ """ Play the requested item """
try: # Python 3
from urllib.parse import unquote
except ImportError: # Python 2
from urllib import unquote
from resources.lib.modules.player import Player from resources.lib.modules.player import Player
Player().play_from_page(unquote(page)) Player().play_from_page(unquote(page))

View File

@ -45,6 +45,7 @@ HTML_MAPPING = [
(re.compile(r'</?(li|ul|ol)(|\s[^>]+)>', re.I), '\n'), (re.compile(r'</?(li|ul|ol)(|\s[^>]+)>', re.I), '\n'),
(re.compile(r'</?(code|div|p|pre|span)(|\s[^>]+)>', re.I), ''), (re.compile(r'</?(code|div|p|pre|span)(|\s[^>]+)>', re.I), ''),
(re.compile('(&nbsp;\n){2,}', re.I), '\n'), # Remove repeating non-blocking spaced newlines (re.compile('(&nbsp;\n){2,}', re.I), '\n'), # Remove repeating non-blocking spaced newlines
(re.compile(' +', re.I), ' '), # Remove double spaces
] ]
STREAM_HLS = 'hls' STREAM_HLS = 'hls'
@ -57,8 +58,7 @@ class TitleItem:
""" This helper object holds all information to be used with Kodi xbmc's ListItem object """ """ This helper object holds all information to be used with Kodi xbmc's ListItem object """
def __init__(self, title, path=None, art_dict=None, info_dict=None, prop_dict=None, stream_dict=None, def __init__(self, title, path=None, art_dict=None, info_dict=None, prop_dict=None, stream_dict=None,
context_menu=None, subtitles_path=None, context_menu=None, subtitles_path=None, is_playable=False, visible=True):
is_playable=False):
""" The constructor for the TitleItem class """ The constructor for the TitleItem class
:type title: str :type title: str
:type path: str :type path: str
@ -69,6 +69,7 @@ class TitleItem:
:type context_menu: list[tuple[str, str]] :type context_menu: list[tuple[str, str]]
:type subtitles_path: list[str] :type subtitles_path: list[str]
:type is_playable: bool :type is_playable: bool
:type visible: bool
""" """
self.title = title self.title = title
self.path = path self.path = path
@ -79,6 +80,7 @@ class TitleItem:
self.context_menu = context_menu self.context_menu = context_menu
self.subtitles_path = subtitles_path self.subtitles_path = subtitles_path
self.is_playable = is_playable self.is_playable = is_playable
self.visible = visible
def __repr__(self): def __repr__(self):
return "%r" % self.__dict__ return "%r" % self.__dict__
@ -189,6 +191,9 @@ def show_listing(title_items, category=None, sort=None, content=None, cache=True
# Add the listings # Add the listings
listing = [] listing = []
for title_item in title_items: for title_item in title_items:
if not title_item.visible:
continue
# Three options: # Three options:
# - item is a virtual directory/folder (not playable, path) # - item is a virtual directory/folder (not playable, path)
# - item is a playable file (playable, path) # - item is a playable file (playable, path)

View File

@ -178,6 +178,73 @@ class Catalog:
# Sort like we get our results back. # Sort like we get our results back.
kodiutils.show_listing(listing, 30003, content='episodes') kodiutils.show_listing(listing, 30003, content='episodes')
def show_categories(self):
""" Shows the categories """
categories = self._api.get_categories()
listing = []
for category in categories:
listing.append(TitleItem(title=category.title,
path=kodiutils.url_for('show_category', category=category.uuid),
info_dict={
'title': category.title,
}))
kodiutils.show_listing(listing, 30003, sort=['title'])
def show_category(self, uuid):
""" Shows a category """
programs = self._api.get_category_content(int(uuid))
listing = [
Menu.generate_titleitem(program) for program in programs
]
kodiutils.show_listing(listing, 30003, content='tvshows')
def show_recommendations(self):
""" Shows the recommendations """
# "Meest bekeken" has a specific API endpoint, the other categories are scraped from the website.
listing = [
TitleItem(title='Meest bekeken',
path=kodiutils.url_for('show_recommendations_category', category='meest-bekeken'),
info_dict={
'title': 'Meest bekeken',
})
]
recommendations = self._api.get_recommendation_categories()
for category in recommendations:
listing.append(TitleItem(title=category.title,
path=kodiutils.url_for('show_recommendations_category', category=category.uuid),
info_dict={
'title': category.title,
}))
kodiutils.show_listing(listing, 30005, content='tvshows')
def show_recommendations_category(self, uuid):
""" Shows the a category of the recommendations """
if uuid == 'meest-bekeken':
programs = self._api.get_popular_programs()
episodes = []
else:
recommendations = self._api.get_recommendation_categories()
category = next(category for category in recommendations if category.uuid == uuid)
programs = category.programs
episodes = category.episodes
listing = []
for episode in episodes:
title_item = Menu.generate_titleitem(episode)
title_item.info_dict['title'] = episode.program_title + ' - ' + title_item.title
listing.append(title_item)
for program in programs:
listing.append(Menu.generate_titleitem(program))
kodiutils.show_listing(listing, 30005, content='tvshows')
def show_mylist(self): def show_mylist(self):
""" Show all the programs of all channels """ """ Show all the programs of all channels """
try: try:

View File

@ -103,20 +103,6 @@ class Channels:
) )
) )
# 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 # Add YouTube channels
if kodiutils.get_cond_visibility('System.HasAddon(plugin.video.youtube)') != 0: if kodiutils.get_cond_visibility('System.HasAddon(plugin.video.youtube)') != 0:
for youtube in channel_info.get('youtube', []): for youtube in channel_info.get('youtube', []):
@ -131,58 +117,3 @@ class Channels:
) )
kodiutils.show_listing(listing, 30007, sort=['unsorted']) 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)
#
# 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(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

@ -8,6 +8,11 @@ from resources.lib.kodiutils import TitleItem
from resources.lib.viervijfzes import STREAM_DICT from resources.lib.viervijfzes import STREAM_DICT
from resources.lib.viervijfzes.content import Episode, Program from resources.lib.viervijfzes.content import Episode, Program
try: # Python 3
from urllib.parse import quote
except ImportError: # Python 2
from urllib import quote
class Menu: class Menu:
""" Menu code """ """ Menu code """
@ -41,6 +46,28 @@ class Menu:
plot=kodiutils.localize(30008), plot=kodiutils.localize(30008),
) )
), ),
TitleItem(
title=kodiutils.localize(30003), # Catalog
path=kodiutils.url_for('show_categories'),
art_dict=dict(
icon='DefaultGenre.png',
fanart=kodiutils.get_addon_info('fanart'),
),
info_dict=dict(
plot=kodiutils.localize(30004),
)
),
TitleItem(
title=kodiutils.localize(30005), # Recommendations
path=kodiutils.url_for('show_recommendations'),
art_dict=dict(
icon='DefaultFavourites.png',
fanart=kodiutils.get_addon_info('fanart'),
),
info_dict=dict(
plot=kodiutils.localize(30006),
)
),
TitleItem( TitleItem(
title=kodiutils.localize(30011), # My List title=kodiutils.localize(30011), # My List
path=kodiutils.url_for('show_mylist'), path=kodiutils.url_for('show_mylist'),
@ -94,9 +121,12 @@ class Menu:
'season': len(item.seasons) if item.seasons else None, 'season': len(item.seasons) if item.seasons else None,
}) })
visible = True
if isinstance(item.episodes, list) and not item.episodes: if isinstance(item.episodes, list) and not item.episodes:
# We know that we don't have episodes # We know that we don't have episodes
title = '[COLOR gray]' + item.title + '[/COLOR]' title = '[COLOR gray]' + item.title + '[/COLOR]'
visible = kodiutils.get_setting_bool('interface_show_unavailable')
else: else:
# We have episodes, or we don't know it # We have episodes, or we don't know it
title = item.title title = item.title
@ -126,7 +156,8 @@ class Menu:
path=kodiutils.url_for('show_catalog_program', program=item.path), path=kodiutils.url_for('show_catalog_program', program=item.path),
context_menu=context_menu, context_menu=context_menu,
art_dict=art_dict, art_dict=art_dict,
info_dict=info_dict) info_dict=info_dict,
visible=visible)
# #
# Episode # Episode
@ -146,11 +177,6 @@ class Menu:
}) })
if item.path: if item.path:
try: # Python 3
from urllib.parse import quote
except ImportError: # Python 2
from urllib import quote
# 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=''))
else: else:

View File

@ -12,6 +12,11 @@ from resources.lib.viervijfzes.auth import AuthApi
from resources.lib.viervijfzes.aws.cognito_idp import AuthenticationException, InvalidLoginException from resources.lib.viervijfzes.aws.cognito_idp import AuthenticationException, InvalidLoginException
from resources.lib.viervijfzes.content import ContentApi, GeoblockedException, UnavailableException from resources.lib.viervijfzes.content import ContentApi, GeoblockedException, UnavailableException
try: # Python 3
from urllib.parse import quote, urlencode
except ImportError: # Python 2
from urllib import quote, urlencode
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -142,11 +147,6 @@ class Player:
:param str key_value: :param str key_value:
:rtype: str :rtype: str
""" """
try: # Python 3
from urllib.parse import quote, urlencode
except ImportError: # Python 2
from urllib import quote, urlencode
header = '' header = ''
if key_headers: if key_headers:
header = urlencode(key_headers) header = urlencode(key_headers)

View File

@ -13,6 +13,11 @@ from resources.lib.viervijfzes import STREAM_DICT
from resources.lib.viervijfzes.content import UnavailableException from resources.lib.viervijfzes.content import UnavailableException
from resources.lib.viervijfzes.epg import EpgApi from resources.lib.viervijfzes.epg import EpgApi
try: # Python 3
from urllib.parse import quote
except ImportError: # Python 2
from urllib import quote
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -104,11 +109,6 @@ class TvGuide:
kodiutils.end_of_directory() kodiutils.end_of_directory()
return return
try: # Python 3
from urllib.parse import quote
except ImportError: # Python 2
from urllib import quote
listing = [] listing = []
for program in programs: for program in programs:
if program.program_url: if program.program_url:

View File

@ -3,6 +3,7 @@
from __future__ import absolute_import, division, unicode_literals from __future__ import absolute_import, division, unicode_literals
import hashlib
import json import json
import logging import logging
import os import os
@ -208,7 +209,7 @@ class ContentApi:
return data return data
# Fetch listing from cache or update if needed # Fetch listing from cache or update if needed
data = self._handle_cache(key=['programs'], cache_mode=cache, update=update, ttl=5 * 60) data = self._handle_cache(key=['programs'], cache_mode=cache, update=update, ttl=30 * 60) # 30 minutes
if not data: if not data:
return [] return []
@ -381,75 +382,145 @@ class ContentApi:
stream_type=STREAM_HLS, stream_type=STREAM_HLS,
) )
# def get_categories(self): def get_program_tree(self, cache=CACHE_AUTO):
# """ Get a list of all categories. """ Get a content tree with information about all the programs.
# :rtype list[Category] :type cache: str
# """ :rtype dict
# # 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
# @staticmethod def update():
# def _extract_programs(html, channel): """ Fetch the content tree """
# """ Extract Programs from HTML code """ response = self._get_url(self.SITE_URL + '/api/content_tree')
# # Item regexes return json.loads(response)
# regex_item = re.compile(r'<a[^>]+?href="(?P<path>[^"]+)"[^>]+?>'
# r'.*?<h3 class="poster-teaser__title"><span>(?P<title>[^<]*)</span></h3>.*?' # Fetch listing from cache or update if needed
# r'</a>', re.DOTALL) data = self._handle_cache(key=['content_tree'], cache_mode=cache, update=update, ttl=5 * 60) # 5 minutes
#
# # Extract items return data
# programs = []
# for item in regex_item.finditer(html): def get_popular_programs(self, brand=None):
# path = item.group('path') """ Get a list of popular programs.
# if path.startswith('/video'): :rtype list[Program]
# continue """
# if brand:
# title = unescape(item.group('title')) response = self._get_url(self.SITE_URL + '/api/programs/popular/%s' % brand)
# else:
# # Program response = self._get_url(self.SITE_URL + '/api/programs/popular')
# programs.append(Program( data = json.loads(response)
# path=path.lstrip('/'),
# channel=channel, programs = []
# title=title, for program in data:
# )) programs.append(self._parse_program_data(program))
#
# return programs return programs
def get_categories(self):
""" Return a list of categories.
:rtype list[Category]
"""
content_tree = self.get_program_tree()
categories = []
for category_id, category_name in content_tree.get('categories').items():
categories.append(Category(uuid=category_id,
title=category_name))
return categories
def get_category_content(self, category_id):
""" Return a category.
:type category_id: int
:rtype list[Program]
"""
content_tree = self.get_program_tree()
# Find out all the program_id's of the requested category
program_ids = [key for key, value in content_tree.get('programs').items() if value.get('category') == category_id]
# Filter out the list of all programs to only keep the one of the requested category
return [program for program in self.get_programs() if program.uuid in program_ids]
def get_recommendation_categories(self):
""" Get a list of all categories.
:rtype list[Category]
"""
# Load all programs
all_programs = self.get_programs()
# Load webpage
raw_html = self._get_url(self.SITE_URL)
# Categories regexes
regex_articles = re.compile(r'<article[^>]+>(.*?)</article>', re.DOTALL)
regex_category = re.compile(r'<h1.*?>(.*?)</h1>(?:.*?<div class="visually-hidden">(.*?)</div>)?', re.DOTALL)
categories = []
for result in regex_articles.finditer(raw_html):
article_html = result.group(1)
match_category = regex_category.search(article_html)
category_title = match_category.group(1).strip()
if match_category.group(2):
category_title += ' [B]%s[/B]' % match_category.group(2).strip()
# Extract programs and lookup in all_programs so we have more metadata
programs = []
for program in self._extract_programs(article_html):
try:
rich_program = next(rich_program for rich_program in all_programs if rich_program.path == program.path)
programs.append(rich_program)
except StopIteration:
programs.append(program)
episodes = self._extract_videos(article_html)
categories.append(
Category(uuid=hashlib.md5(category_title.encode('utf-8')).hexdigest(), title=category_title, programs=programs, episodes=episodes))
return categories
@staticmethod
def _extract_programs(html):
""" Extract Programs from HTML code
:type html: str
: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>.*?)".*?'
r'</a>', re.DOTALL)
# Extract items
programs = []
for item in regex_item.finditer(html):
path = item.group('path')
if path.startswith('/video'):
continue
# Program
programs.append(Program(
path=path.lstrip('/'),
title=unescape(item.group('title')),
cover=unescape(item.group('image')),
))
return programs
@staticmethod @staticmethod
def _extract_videos(html): def _extract_videos(html):
""" Extract videos from HTML code """ """ Extract videos from HTML code
:type html: str
: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[^>]+?href="(?P<path>[^"]+)"[^>]+?>.*?</a>', re.DOTALL)
# Episode regexes regex_episode_program = re.compile(r'<h3 class="episode-teaser__subtitle">([^<]*)</h3>')
regex_episode_title = re.compile(r'<h3 class="(?:poster|card|image)-teaser__title">(?:<span>)?([^<]*)(?:</span>)?</h3>') regex_episode_title = re.compile(r'<(?:div|h3) class="(?:poster|card|image|episode)-teaser__title">(?:<span>)?([^<]*)(?:</span>)?</(?:div|h3)>')
regex_episode_program = re.compile(r'<div class="card-teaser__label">([^<]*)</div>')
regex_episode_duration = re.compile(r'data-duration="([^"]*)"') regex_episode_duration = re.compile(r'data-duration="([^"]*)"')
regex_episode_video_id = re.compile(r'data-videoid="([^"]*)"') 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'data-background-image="([^"]*)"')
regex_episode_timestamp = re.compile(r'data-timestamp="([^"]*)"') regex_episode_badge = re.compile(r'<div class="(?:poster|card|image|episode)-teaser__badge badge">([^<]*)</div>')
# Extract items # Extract items
episodes = [] episodes = []
@ -463,7 +534,7 @@ class ContentApi:
except AttributeError: except AttributeError:
continue continue
# This is not a episode # This is not a video
if not path.startswith('/video'): if not path.startswith('/video'):
continue continue
@ -472,35 +543,42 @@ class ContentApi:
except AttributeError: except AttributeError:
_LOGGER.warning('Found no episode_program for %s', title) _LOGGER.warning('Found no episode_program for %s', title)
episode_program = None episode_program = None
try: try:
episode_duration = int(regex_episode_duration.search(item_html).group(1)) episode_duration = int(regex_episode_duration.search(item_html).group(1))
except AttributeError: except AttributeError:
_LOGGER.warning('Found no episode_duration for %s', title) _LOGGER.warning('Found no episode_duration for %s', title)
episode_duration = None episode_duration = None
try: try:
episode_video_id = regex_episode_video_id.search(item_html).group(1) episode_video_id = regex_episode_video_id.search(item_html).group(1)
except AttributeError: except AttributeError:
_LOGGER.warning('Found no episode_video_id for %s', title) _LOGGER.warning('Found no episode_video_id for %s', title)
episode_video_id = None episode_video_id = None
try: try:
episode_image = unescape(regex_episode_image.search(item_html).group(1)) episode_image = unescape(regex_episode_image.search(item_html).group(1))
except AttributeError: except AttributeError:
_LOGGER.warning('Found no episode_image for %s', title) _LOGGER.warning('Found no episode_image for %s', title)
episode_image = None episode_image = None
try: try:
episode_timestamp = int(regex_episode_timestamp.search(item_html).group(1)) episode_badge = unescape(regex_episode_badge.search(item_html).group(1))
except AttributeError: except AttributeError:
_LOGGER.warning('Found no episode_timestamp for %s', title) episode_badge = None
episode_timestamp = None
description = title
if episode_badge:
description += "\n\n[B]%s[/B]" % episode_badge
# Episode # Episode
episodes.append(Episode( episodes.append(Episode(
path=path.lstrip('/'), path=path.lstrip('/'),
channel='', # TODO channel='', # TODO
title=title, title=title,
description=html_to_kodi(description),
duration=episode_duration, duration=episode_duration,
uuid=episode_video_id, uuid=episode_video_id,
aired=datetime.fromtimestamp(episode_timestamp) if episode_timestamp else None,
cover=episode_image, cover=episode_image,
program_title=episode_program, program_title=episode_program,
)) ))
@ -519,7 +597,7 @@ class ContentApi:
path=data['link'].lstrip('/'), path=data['link'].lstrip('/'),
channel=data['pageInfo']['brand'], channel=data['pageInfo']['brand'],
title=data['title'], title=data['title'],
description=data['description'], description=html_to_kodi(data['description']),
aired=datetime.fromtimestamp(data.get('pageInfo', {}).get('publishDate')), aired=datetime.fromtimestamp(data.get('pageInfo', {}).get('publishDate')),
cover=data['images']['poster'], cover=data['images']['poster'],
background=data['images']['hero'], background=data['images']['hero'],

View File

@ -5,6 +5,10 @@
<setting label="30802" type="text" id="username"/> <setting label="30802" type="text" id="username"/>
<setting label="30803" type="text" id="password" option="hidden"/> <setting label="30803" type="text" id="password" option="hidden"/>
</category> </category>
<category label="30820"> <!-- Interface -->
<setting label="30820" type="lsep"/> <!-- Interface -->
<setting label="30821" type="bool" id="interface_show_unavailable" default="true"/>
</category>
<category label="30840"> <!-- Integrations --> <category label="30840"> <!-- Integrations -->
<setting label="30841" type="lsep"/> <!-- IPTV Manager --> <setting label="30841" type="lsep"/> <!-- IPTV Manager -->
<setting label="30842" type="action" action="InstallAddon(service.iptv.manager)" option="close" visible="!System.HasAddon(service.iptv.manager)"/> <!-- Install IPTV Manager add-on --> <setting label="30842" type="action" action="InstallAddon(service.iptv.manager)" option="close" visible="!System.HasAddon(service.iptv.manager)"/> <!-- Install IPTV Manager add-on -->

View File

@ -11,7 +11,7 @@ import unittest
import resources.lib.kodiutils as kodiutils import resources.lib.kodiutils as kodiutils
from resources.lib.viervijfzes import ResolvedStream from resources.lib.viervijfzes import ResolvedStream
from resources.lib.viervijfzes.auth import AuthApi from resources.lib.viervijfzes.auth import AuthApi
from resources.lib.viervijfzes.content import ContentApi, Program, Episode, CACHE_PREVENT from resources.lib.viervijfzes.content import ContentApi, Program, Episode, CACHE_PREVENT, Category
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -27,10 +27,24 @@ class TestApi(unittest.TestCase):
self.assertIsInstance(programs, list) self.assertIsInstance(programs, list)
self.assertIsInstance(programs[0], Program) self.assertIsInstance(programs[0], Program)
# def test_categories(self): def test_popular_programs(self):
# categories = self._api.get_categories() for brand in [None, 'vier', 'vijf', 'zes', 'goplay']:
# self.assertIsInstance(categories, list) programs = self._api.get_popular_programs(brand)
# self.assertIsInstance(categories[0], Category) self.assertIsInstance(programs, list)
self.assertIsInstance(programs[0], Program)
def test_recommendations(self):
categories = self._api.get_recommendation_categories()
self.assertIsInstance(categories, list)
def test_categories(self):
categories = self._api.get_categories()
self.assertIsInstance(categories, list)
self.assertIsInstance(categories[0], Category)
programs = self._api.get_category_content(int(categories[0].uuid))
self.assertIsInstance(programs, list)
self.assertIsInstance(programs[0], Program)
def test_episodes(self): def test_episodes(self):
for program in ['auwch', 'zo-man-zo-vrouw']: for program in ['auwch', 'zo-man-zo-vrouw']:

View File

@ -34,6 +34,9 @@ class TestRouting(unittest.TestCase):
def test_catalog_menu(self): def test_catalog_menu(self):
routing.run([routing.url_for(addon.show_catalog), '0', '']) routing.run([routing.url_for(addon.show_catalog), '0', ''])
def test_recommendations_menu(self):
routing.run([routing.url_for(addon.show_recommendations), '0', ''])
def test_catalog_channel_menu(self): def test_catalog_channel_menu(self):
routing.run([routing.url_for(addon.show_channel_catalog, channel='Play4'), '0', '']) routing.run([routing.url_for(addon.show_channel_catalog, channel='Play4'), '0', ''])