Add recommendations and categories (#76)
This commit is contained in:
parent
9ddc73094d
commit
88e1bbc4d6
@ -18,6 +18,18 @@ msgctxt "#30003"
|
||||
msgid "Catalogue"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30004"
|
||||
msgid "Browse the catalogue"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30005"
|
||||
msgid "Recommendations"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30006"
|
||||
msgid "Show the recommendations"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30007"
|
||||
msgid "Channels"
|
||||
msgstr ""
|
||||
@ -64,14 +76,6 @@ msgctxt "#30056"
|
||||
msgid "Browse the Catalog for [B]{channel}[/B]"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30057"
|
||||
msgid "Categories for [B]{channel}[/B]"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30058"
|
||||
msgid "Browse the Categories for [B]{channel}[/B]"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30059"
|
||||
msgid "Clips of [B]{program}[/B]"
|
||||
msgstr ""
|
||||
@ -186,6 +190,14 @@ msgctxt "#30803"
|
||||
msgid "Password"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30820"
|
||||
msgid "Interface"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30821"
|
||||
msgid "Show unavailable programs"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30840"
|
||||
msgid "Integration"
|
||||
msgstr ""
|
||||
|
@ -19,6 +19,18 @@ msgctxt "#30003"
|
||||
msgid "Catalogue"
|
||||
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"
|
||||
msgid "Channels"
|
||||
msgstr "Kanalen"
|
||||
@ -65,14 +77,6 @@ msgctxt "#30056"
|
||||
msgid "Browse the Catalog for [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"
|
||||
msgid "Clips of [B]{program}[/B]"
|
||||
msgstr "Clips van [B]{program}[/B]"
|
||||
@ -187,6 +191,14 @@ msgctxt "#30803"
|
||||
msgid "Password"
|
||||
msgstr "Wachtwoord"
|
||||
|
||||
msgctxt "#30820"
|
||||
msgid "Interface"
|
||||
msgstr "Interface"
|
||||
|
||||
msgctxt "#30821"
|
||||
msgid "Show unavailable programs"
|
||||
msgstr "Toon onbeschikbare programma's"
|
||||
|
||||
msgctxt "#30840"
|
||||
msgid "Integration"
|
||||
msgstr "Integratie"
|
||||
|
@ -9,6 +9,11 @@ from routing import Plugin
|
||||
|
||||
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
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -34,20 +39,6 @@ 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/<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')
|
||||
def show_channel_tvguide(channel):
|
||||
""" Shows the dates in the tv guide """
|
||||
@ -97,6 +88,34 @@ def show_catalog_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')
|
||||
def show_mylist():
|
||||
""" Show my list """
|
||||
@ -150,11 +169,6 @@ def play_catalog(uuid):
|
||||
@routing.route('/play/page/<page>')
|
||||
def play_from_page(page):
|
||||
""" 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
|
||||
Player().play_from_page(unquote(page))
|
||||
|
||||
|
@ -45,6 +45,7 @@ HTML_MAPPING = [
|
||||
(re.compile(r'</?(li|ul|ol)(|\s[^>]+)>', re.I), '\n'),
|
||||
(re.compile(r'</?(code|div|p|pre|span)(|\s[^>]+)>', re.I), ''),
|
||||
(re.compile('( \n){2,}', re.I), '\n'), # Remove repeating non-blocking spaced newlines
|
||||
(re.compile(' +', re.I), ' '), # Remove double spaces
|
||||
]
|
||||
|
||||
STREAM_HLS = 'hls'
|
||||
@ -57,8 +58,7 @@ class TitleItem:
|
||||
""" 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,
|
||||
context_menu=None, subtitles_path=None,
|
||||
is_playable=False):
|
||||
context_menu=None, subtitles_path=None, is_playable=False, visible=True):
|
||||
""" The constructor for the TitleItem class
|
||||
:type title: str
|
||||
:type path: str
|
||||
@ -69,6 +69,7 @@ class TitleItem:
|
||||
:type context_menu: list[tuple[str, str]]
|
||||
:type subtitles_path: list[str]
|
||||
:type is_playable: bool
|
||||
:type visible: bool
|
||||
"""
|
||||
self.title = title
|
||||
self.path = path
|
||||
@ -79,6 +80,7 @@ class TitleItem:
|
||||
self.context_menu = context_menu
|
||||
self.subtitles_path = subtitles_path
|
||||
self.is_playable = is_playable
|
||||
self.visible = visible
|
||||
|
||||
def __repr__(self):
|
||||
return "%r" % self.__dict__
|
||||
@ -189,6 +191,9 @@ def show_listing(title_items, category=None, sort=None, content=None, cache=True
|
||||
# Add the listings
|
||||
listing = []
|
||||
for title_item in title_items:
|
||||
if not title_item.visible:
|
||||
continue
|
||||
|
||||
# Three options:
|
||||
# - item is a virtual directory/folder (not playable, path)
|
||||
# - item is a playable file (playable, path)
|
||||
|
@ -178,6 +178,73 @@ class Catalog:
|
||||
# Sort like we get our results back.
|
||||
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):
|
||||
""" Show all the programs of all channels """
|
||||
try:
|
||||
|
@ -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
|
||||
if kodiutils.get_cond_visibility('System.HasAddon(plugin.video.youtube)') != 0:
|
||||
for youtube in channel_info.get('youtube', []):
|
||||
@ -131,58 +117,3 @@ 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)
|
||||
#
|
||||
# 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'])
|
||||
|
@ -8,6 +8,11 @@ from resources.lib.kodiutils import TitleItem
|
||||
from resources.lib.viervijfzes import STREAM_DICT
|
||||
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:
|
||||
""" Menu code """
|
||||
@ -41,6 +46,28 @@ class Menu:
|
||||
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(
|
||||
title=kodiutils.localize(30011), # My List
|
||||
path=kodiutils.url_for('show_mylist'),
|
||||
@ -94,9 +121,12 @@ class Menu:
|
||||
'season': len(item.seasons) if item.seasons else None,
|
||||
})
|
||||
|
||||
visible = True
|
||||
if isinstance(item.episodes, list) and not item.episodes:
|
||||
# We know that we don't have episodes
|
||||
title = '[COLOR gray]' + item.title + '[/COLOR]'
|
||||
visible = kodiutils.get_setting_bool('interface_show_unavailable')
|
||||
|
||||
else:
|
||||
# We have episodes, or we don't know it
|
||||
title = item.title
|
||||
@ -126,7 +156,8 @@ class Menu:
|
||||
path=kodiutils.url_for('show_catalog_program', program=item.path),
|
||||
context_menu=context_menu,
|
||||
art_dict=art_dict,
|
||||
info_dict=info_dict)
|
||||
info_dict=info_dict,
|
||||
visible=visible)
|
||||
|
||||
#
|
||||
# Episode
|
||||
@ -146,11 +177,6 @@ class Menu:
|
||||
})
|
||||
|
||||
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
|
||||
path = kodiutils.url_for('play_from_page', page=quote(item.path, safe=''))
|
||||
else:
|
||||
|
@ -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.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__)
|
||||
|
||||
|
||||
@ -142,11 +147,6 @@ class Player:
|
||||
:param str key_value:
|
||||
:rtype: str
|
||||
"""
|
||||
try: # Python 3
|
||||
from urllib.parse import quote, urlencode
|
||||
except ImportError: # Python 2
|
||||
from urllib import quote, urlencode
|
||||
|
||||
header = ''
|
||||
if key_headers:
|
||||
header = urlencode(key_headers)
|
||||
|
@ -13,6 +13,11 @@ from resources.lib.viervijfzes import STREAM_DICT
|
||||
from resources.lib.viervijfzes.content import UnavailableException
|
||||
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__)
|
||||
|
||||
|
||||
@ -104,11 +109,6 @@ class TvGuide:
|
||||
kodiutils.end_of_directory()
|
||||
return
|
||||
|
||||
try: # Python 3
|
||||
from urllib.parse import quote
|
||||
except ImportError: # Python 2
|
||||
from urllib import quote
|
||||
|
||||
listing = []
|
||||
for program in programs:
|
||||
if program.program_url:
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
@ -208,7 +209,7 @@ class ContentApi:
|
||||
return data
|
||||
|
||||
# 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:
|
||||
return []
|
||||
|
||||
@ -381,75 +382,145 @@ class ContentApi:
|
||||
stream_type=STREAM_HLS,
|
||||
)
|
||||
|
||||
# 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
|
||||
def get_program_tree(self, cache=CACHE_AUTO):
|
||||
""" Get a content tree with information about all the programs.
|
||||
:type cache: str
|
||||
:rtype dict
|
||||
"""
|
||||
|
||||
# @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
|
||||
def update():
|
||||
""" Fetch the content tree """
|
||||
response = self._get_url(self.SITE_URL + '/api/content_tree')
|
||||
return json.loads(response)
|
||||
|
||||
# Fetch listing from cache or update if needed
|
||||
data = self._handle_cache(key=['content_tree'], cache_mode=cache, update=update, ttl=5 * 60) # 5 minutes
|
||||
|
||||
return data
|
||||
|
||||
def get_popular_programs(self, brand=None):
|
||||
""" Get a list of popular programs.
|
||||
:rtype list[Program]
|
||||
"""
|
||||
if brand:
|
||||
response = self._get_url(self.SITE_URL + '/api/programs/popular/%s' % brand)
|
||||
else:
|
||||
response = self._get_url(self.SITE_URL + '/api/programs/popular')
|
||||
data = json.loads(response)
|
||||
|
||||
programs = []
|
||||
for program in data:
|
||||
programs.append(self._parse_program_data(program))
|
||||
|
||||
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
|
||||
def _extract_videos(html):
|
||||
""" Extract videos from HTML code """
|
||||
""" Extract videos from HTML code
|
||||
:type html: str
|
||||
:rtype list[Episode]
|
||||
"""
|
||||
# Item regexes
|
||||
regex_item = re.compile(r'<a[^>]+?href="(?P<path>[^"]+)"[^>]+?>.*?</a>', re.DOTALL)
|
||||
|
||||
# Episode regexes
|
||||
regex_episode_title = re.compile(r'<h3 class="(?:poster|card|image)-teaser__title">(?:<span>)?([^<]*)(?:</span>)?</h3>')
|
||||
regex_episode_program = re.compile(r'<div class="card-teaser__label">([^<]*)</div>')
|
||||
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-videoid="([^"]*)"')
|
||||
regex_episode_video_id = re.compile(r'data-video-id="([^"]*)"')
|
||||
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
|
||||
episodes = []
|
||||
@ -463,7 +534,7 @@ class ContentApi:
|
||||
except AttributeError:
|
||||
continue
|
||||
|
||||
# This is not a episode
|
||||
# This is not a video
|
||||
if not path.startswith('/video'):
|
||||
continue
|
||||
|
||||
@ -472,35 +543,42 @@ class ContentApi:
|
||||
except AttributeError:
|
||||
_LOGGER.warning('Found no episode_program for %s', title)
|
||||
episode_program = None
|
||||
|
||||
try:
|
||||
episode_duration = int(regex_episode_duration.search(item_html).group(1))
|
||||
except AttributeError:
|
||||
_LOGGER.warning('Found no episode_duration for %s', title)
|
||||
episode_duration = None
|
||||
|
||||
try:
|
||||
episode_video_id = regex_episode_video_id.search(item_html).group(1)
|
||||
except AttributeError:
|
||||
_LOGGER.warning('Found no episode_video_id for %s', title)
|
||||
episode_video_id = None
|
||||
|
||||
try:
|
||||
episode_image = unescape(regex_episode_image.search(item_html).group(1))
|
||||
except AttributeError:
|
||||
_LOGGER.warning('Found no episode_image for %s', title)
|
||||
episode_image = None
|
||||
|
||||
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:
|
||||
_LOGGER.warning('Found no episode_timestamp for %s', title)
|
||||
episode_timestamp = None
|
||||
episode_badge = None
|
||||
|
||||
description = title
|
||||
if episode_badge:
|
||||
description += "\n\n[B]%s[/B]" % episode_badge
|
||||
|
||||
# Episode
|
||||
episodes.append(Episode(
|
||||
path=path.lstrip('/'),
|
||||
channel='', # TODO
|
||||
title=title,
|
||||
description=html_to_kodi(description),
|
||||
duration=episode_duration,
|
||||
uuid=episode_video_id,
|
||||
aired=datetime.fromtimestamp(episode_timestamp) if episode_timestamp else None,
|
||||
cover=episode_image,
|
||||
program_title=episode_program,
|
||||
))
|
||||
@ -519,7 +597,7 @@ class ContentApi:
|
||||
path=data['link'].lstrip('/'),
|
||||
channel=data['pageInfo']['brand'],
|
||||
title=data['title'],
|
||||
description=data['description'],
|
||||
description=html_to_kodi(data['description']),
|
||||
aired=datetime.fromtimestamp(data.get('pageInfo', {}).get('publishDate')),
|
||||
cover=data['images']['poster'],
|
||||
background=data['images']['hero'],
|
||||
|
@ -5,6 +5,10 @@
|
||||
<setting label="30802" type="text" id="username"/>
|
||||
<setting label="30803" type="text" id="password" option="hidden"/>
|
||||
</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 -->
|
||||
<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 -->
|
||||
|
@ -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, CACHE_PREVENT
|
||||
from resources.lib.viervijfzes.content import ContentApi, Program, Episode, CACHE_PREVENT, Category
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -27,10 +27,24 @@ class TestApi(unittest.TestCase):
|
||||
self.assertIsInstance(programs, list)
|
||||
self.assertIsInstance(programs[0], Program)
|
||||
|
||||
# def test_categories(self):
|
||||
# categories = self._api.get_categories()
|
||||
# self.assertIsInstance(categories, list)
|
||||
# self.assertIsInstance(categories[0], Category)
|
||||
def test_popular_programs(self):
|
||||
for brand in [None, 'vier', 'vijf', 'zes', 'goplay']:
|
||||
programs = self._api.get_popular_programs(brand)
|
||||
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):
|
||||
for program in ['auwch', 'zo-man-zo-vrouw']:
|
||||
|
@ -34,6 +34,9 @@ class TestRouting(unittest.TestCase):
|
||||
def test_catalog_menu(self):
|
||||
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):
|
||||
routing.run([routing.url_for(addon.show_channel_catalog, channel='Play4'), '0', ''])
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user