Add recommendations and categories (#76)
This commit is contained in:
parent
9ddc73094d
commit
88e1bbc4d6
@ -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 ""
|
||||||
|
@ -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"
|
||||||
|
@ -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))
|
||||||
|
|
||||||
|
@ -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('( \n){2,}', re.I), '\n'), # Remove repeating non-blocking spaced newlines
|
(re.compile('( \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)
|
||||||
|
@ -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:
|
||||||
|
@ -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'])
|
|
||||||
|
@ -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:
|
||||||
|
@ -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)
|
||||||
|
@ -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:
|
||||||
|
@ -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'],
|
||||||
|
@ -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 -->
|
||||||
|
@ -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']:
|
||||||
|
@ -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', ''])
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user