Add categories and clips (#23)
This commit is contained in:
parent
40af262ae6
commit
b33062bd35
@ -64,6 +64,22 @@ 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"
|
||||||
|
msgid "Clips of [B]{program}[/B]"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30060"
|
||||||
|
msgid "Watch short clips of [B]{program}[/B]"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
|
||||||
### CONTEXT MENU
|
### CONTEXT MENU
|
||||||
msgctxt "#30102"
|
msgctxt "#30102"
|
||||||
|
@ -65,6 +65,22 @@ 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"
|
||||||
|
msgid "Clips of [B]{program}[/B]"
|
||||||
|
msgstr "Clips van [B]{program}[/B]"
|
||||||
|
|
||||||
|
msgctxt "#30060"
|
||||||
|
msgid "Watch short clips of [B]{program}[/B]"
|
||||||
|
msgstr "Bekijk korte videoclips van [B]{program}[/B]"
|
||||||
|
|
||||||
|
|
||||||
### CONTEXT MENU
|
### CONTEXT MENU
|
||||||
msgctxt "#30102"
|
msgctxt "#30102"
|
||||||
|
@ -35,18 +35,32 @@ def show_channel_menu(channel):
|
|||||||
Channels().show_channel_menu(channel)
|
Channels().show_channel_menu(channel)
|
||||||
|
|
||||||
|
|
||||||
@routing.route('/tvguide/channel/<channel>')
|
@routing.route('/channels/<channel>/categories')
|
||||||
def show_tvguide_channel(channel):
|
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 """
|
""" Shows the dates in the tv guide """
|
||||||
from resources.lib.modules.tvguide import TvGuide
|
from resources.lib.modules.tvguide import TvGuide
|
||||||
TvGuide().show_tvguide_channel(channel)
|
TvGuide().show_channel(channel)
|
||||||
|
|
||||||
|
|
||||||
@routing.route('/tvguide/channel/<channel>/<date>')
|
@routing.route('/channels/<channel>/tvguide/<date>')
|
||||||
def show_tvguide_detail(channel=None, date=None):
|
def show_channel_tvguide_detail(channel=None, date=None):
|
||||||
""" Shows the programs of a specific date in the tv guide """
|
""" Shows the programs of a specific date in the tv guide """
|
||||||
from resources.lib.modules.tvguide import TvGuide
|
from resources.lib.modules.tvguide import TvGuide
|
||||||
TvGuide().show_tvguide_detail(channel, date)
|
TvGuide().show_detail(channel, date)
|
||||||
|
|
||||||
|
|
||||||
@routing.route('/catalog')
|
@routing.route('/catalog')
|
||||||
@ -56,23 +70,30 @@ def show_catalog():
|
|||||||
Catalog().show_catalog()
|
Catalog().show_catalog()
|
||||||
|
|
||||||
|
|
||||||
@routing.route('/catalog/by-channel/<channel>')
|
@routing.route('/catalog/<channel>')
|
||||||
def show_catalog_channel(channel):
|
def show_channel_catalog(channel):
|
||||||
""" Show a category in the catalog """
|
""" Show a category in the catalog """
|
||||||
from resources.lib.modules.catalog import Catalog
|
from resources.lib.modules.catalog import Catalog
|
||||||
Catalog().show_catalog_channel(channel)
|
Catalog().show_catalog_channel(channel)
|
||||||
|
|
||||||
|
|
||||||
@routing.route('/catalog/program/<channel>/<program>')
|
@routing.route('/catalog/<channel>/<program>')
|
||||||
def show_catalog_program(channel, program):
|
def show_catalog_program(channel, program):
|
||||||
""" Show a program from the catalog """
|
""" Show a program from the catalog """
|
||||||
from resources.lib.modules.catalog import Catalog
|
from resources.lib.modules.catalog import Catalog
|
||||||
Catalog().show_program(channel, program)
|
Catalog().show_program(channel, program)
|
||||||
|
|
||||||
|
|
||||||
@routing.route('/catalog/program/<channel>/<program>/<season>')
|
@routing.route('/catalog/<channel>/<program>/clips')
|
||||||
|
def show_catalog_program_clips(channel, program):
|
||||||
|
""" Show the clips from a program """
|
||||||
|
from resources.lib.modules.catalog import Catalog
|
||||||
|
Catalog().show_program_clips(channel, program)
|
||||||
|
|
||||||
|
|
||||||
|
@routing.route('/catalog/<channel>/<program>/season/<season>')
|
||||||
def show_catalog_program_season(channel, program, season):
|
def show_catalog_program_season(channel, program, season):
|
||||||
""" Show a program from the catalog """
|
""" Show a season from a program """
|
||||||
from resources.lib.modules.catalog import Catalog
|
from resources.lib.modules.catalog import Catalog
|
||||||
Catalog().show_program_season(channel, program, season)
|
Catalog().show_program_season(channel, program, season)
|
||||||
|
|
||||||
|
@ -22,7 +22,6 @@ class Catalog:
|
|||||||
""" Initialise object """
|
""" Initialise object """
|
||||||
auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password'), kodiutils.get_tokens_path())
|
auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password'), kodiutils.get_tokens_path())
|
||||||
self._api = ContentApi(auth, cache_path=kodiutils.get_cache_path())
|
self._api = ContentApi(auth, cache_path=kodiutils.get_cache_path())
|
||||||
self._menu = Menu()
|
|
||||||
|
|
||||||
def show_catalog(self):
|
def show_catalog(self):
|
||||||
""" Show all the programs of all channels """
|
""" Show all the programs of all channels """
|
||||||
@ -34,7 +33,7 @@ class Catalog:
|
|||||||
kodiutils.notification(message=str(ex))
|
kodiutils.notification(message=str(ex))
|
||||||
raise
|
raise
|
||||||
|
|
||||||
listing = [self._menu.generate_titleitem(item) for item in items]
|
listing = [Menu.generate_titleitem(item) for item in items]
|
||||||
|
|
||||||
# Sort items by title
|
# Sort items by title
|
||||||
# Used for A-Z listing or when movies and episodes are mixed.
|
# Used for A-Z listing or when movies and episodes are mixed.
|
||||||
@ -52,7 +51,7 @@ class Catalog:
|
|||||||
|
|
||||||
listing = []
|
listing = []
|
||||||
for item in items:
|
for item in items:
|
||||||
listing.append(self._menu.generate_titleitem(item))
|
listing.append(Menu.generate_titleitem(item))
|
||||||
|
|
||||||
# Sort items by title
|
# Sort items by title
|
||||||
# Used for A-Z listing or when movies and episodes are mixed.
|
# Used for A-Z listing or when movies and episodes are mixed.
|
||||||
@ -64,19 +63,19 @@ class Catalog:
|
|||||||
:type program_id: str
|
:type program_id: str
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
program = self._api.get_program(channel, program_id, cache=CACHE_PREVENT) # Use CACHE_PREVENT since we want fresh data
|
program = self._api.get_program(channel, program_id, extract_clips=True, cache=CACHE_PREVENT) # Use CACHE_PREVENT since we want fresh data
|
||||||
except UnavailableException:
|
except UnavailableException:
|
||||||
kodiutils.ok_dialog(message=kodiutils.localize(30717)) # This program is not available in the catalogue.
|
kodiutils.ok_dialog(message=kodiutils.localize(30717)) # This program is not available in the catalogue.
|
||||||
kodiutils.end_of_directory()
|
kodiutils.end_of_directory()
|
||||||
return
|
return
|
||||||
|
|
||||||
if not program.episodes:
|
if not program.episodes and not program.clips:
|
||||||
kodiutils.ok_dialog(message=kodiutils.localize(30717)) # This program is not available in the catalogue.
|
kodiutils.ok_dialog(message=kodiutils.localize(30717)) # This program is not available in the catalogue.
|
||||||
kodiutils.end_of_directory()
|
kodiutils.end_of_directory()
|
||||||
return
|
return
|
||||||
|
|
||||||
# Go directly to the season when we have only one season
|
# Go directly to the season when we have only one season and no clips
|
||||||
if len(program.seasons) == 1:
|
if not program.clips and len(program.seasons) == 1:
|
||||||
self.show_program_season(channel, program_id, list(program.seasons.values())[0].uuid)
|
self.show_program_season(channel, program_id, list(program.seasons.values())[0].uuid)
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -85,7 +84,7 @@ class Catalog:
|
|||||||
listing = []
|
listing = []
|
||||||
|
|
||||||
# Add an '* All seasons' entry when configured in Kodi
|
# Add an '* All seasons' entry when configured in Kodi
|
||||||
if kodiutils.get_global_setting('videolibrary.showallitems') is True:
|
if program.seasons and kodiutils.get_global_setting('videolibrary.showallitems') is True:
|
||||||
listing.append(
|
listing.append(
|
||||||
TitleItem(
|
TitleItem(
|
||||||
title='* %s' % kodiutils.localize(30204), # * All seasons
|
title='* %s' % kodiutils.localize(30204), # * All seasons
|
||||||
@ -122,6 +121,25 @@ class Catalog:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Add Clips
|
||||||
|
if program.clips:
|
||||||
|
listing.append(
|
||||||
|
TitleItem(
|
||||||
|
title=kodiutils.localize(30059, program=program.title), # Clips for {program}
|
||||||
|
path=kodiutils.url_for('show_catalog_program_clips', channel=channel, program=program_id),
|
||||||
|
art_dict={
|
||||||
|
'fanart': program.background,
|
||||||
|
},
|
||||||
|
info_dict={
|
||||||
|
'tvshowtitle': program.title,
|
||||||
|
'title': kodiutils.localize(30059, program=program.title), # Clips for {program}
|
||||||
|
'plot': kodiutils.localize(30060, program=program.title), # Watch short clips of {program}
|
||||||
|
'set': program.title,
|
||||||
|
'studio': studio,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# Sort by label. Some programs return seasons unordered.
|
# Sort by label. Some programs return seasons unordered.
|
||||||
kodiutils.show_listing(listing, 30003, content='tvshows')
|
kodiutils.show_listing(listing, 30003, content='tvshows')
|
||||||
|
|
||||||
@ -145,7 +163,25 @@ class Catalog:
|
|||||||
# Show the episodes of the season that was selected
|
# Show the episodes of the season that was selected
|
||||||
episodes = [e for e in program.episodes if e.season_uuid == season_uuid]
|
episodes = [e for e in program.episodes if e.season_uuid == season_uuid]
|
||||||
|
|
||||||
listing = [self._menu.generate_titleitem(episode) for episode in episodes]
|
listing = [Menu.generate_titleitem(episode) for episode in episodes]
|
||||||
|
|
||||||
# Sort by episode number by default. Takes seasons into account.
|
# Sort by episode number by default. Takes seasons into account.
|
||||||
kodiutils.show_listing(listing, 30003, content='episodes', sort=['episode', 'duration'])
|
kodiutils.show_listing(listing, 30003, content='episodes', sort=['episode', 'duration'])
|
||||||
|
|
||||||
|
def show_program_clips(self, channel, program_id):
|
||||||
|
""" Show the clips of a program from the catalog
|
||||||
|
:type channel: str
|
||||||
|
:type program_id: str
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# We need to query the backend, since we don't cache clips.
|
||||||
|
program = self._api.get_program(channel, program_id, extract_clips=True, cache=CACHE_PREVENT)
|
||||||
|
except UnavailableException:
|
||||||
|
kodiutils.ok_dialog(message=kodiutils.localize(30717)) # This program is not available in the catalogue.
|
||||||
|
kodiutils.end_of_directory()
|
||||||
|
return
|
||||||
|
|
||||||
|
listing = [Menu.generate_titleitem(episode) for episode in program.clips]
|
||||||
|
|
||||||
|
# Sort like we get our results back.
|
||||||
|
kodiutils.show_listing(listing, 30003, content='episodes')
|
||||||
|
@ -7,7 +7,10 @@ import logging
|
|||||||
|
|
||||||
from resources.lib import kodiutils
|
from resources.lib import kodiutils
|
||||||
from resources.lib.kodiutils import TitleItem
|
from resources.lib.kodiutils import TitleItem
|
||||||
|
from resources.lib.modules.menu import Menu
|
||||||
from resources.lib.viervijfzes import CHANNELS, STREAM_DICT
|
from resources.lib.viervijfzes import CHANNELS, STREAM_DICT
|
||||||
|
from resources.lib.viervijfzes.auth import AuthApi
|
||||||
|
from resources.lib.viervijfzes.content import ContentApi, CACHE_ONLY, CACHE_AUTO
|
||||||
|
|
||||||
_LOGGER = logging.getLogger('channels')
|
_LOGGER = logging.getLogger('channels')
|
||||||
|
|
||||||
@ -17,6 +20,8 @@ class Channels:
|
|||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
""" Initialise object """
|
""" Initialise object """
|
||||||
|
auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password'), kodiutils.get_tokens_path())
|
||||||
|
self._api = ContentApi(auth, cache_path=kodiutils.get_cache_path())
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def show_channels():
|
def show_channels():
|
||||||
@ -33,7 +38,7 @@ class Channels:
|
|||||||
(
|
(
|
||||||
kodiutils.localize(30053, channel=channel.get('name')), # TV Guide for {channel}
|
kodiutils.localize(30053, channel=channel.get('name')), # TV Guide for {channel}
|
||||||
'Container.Update(%s)' %
|
'Container.Update(%s)' %
|
||||||
kodiutils.url_for('show_tvguide_channel', channel=channel.get('epg'))
|
kodiutils.url_for('show_channel_tvguide', channel=channel.get('epg'))
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -60,43 +65,54 @@ class Channels:
|
|||||||
kodiutils.show_listing(listing, 30007)
|
kodiutils.show_listing(listing, 30007)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def show_channel_menu(key):
|
def show_channel_menu(channel):
|
||||||
""" Shows a TV channel
|
""" Shows a TV channel
|
||||||
:type key: str
|
:type channel: str
|
||||||
"""
|
"""
|
||||||
channel = CHANNELS[key]
|
channel_info = CHANNELS[channel]
|
||||||
|
|
||||||
# Lookup the high resolution logo based on the channel name
|
# Lookup the high resolution logo based on the channel name
|
||||||
fanart = '{path}/resources/logos/{logo}'.format(path=kodiutils.addon_path(), logo=channel.get('background'))
|
fanart = '{path}/resources/logos/{logo}'.format(path=kodiutils.addon_path(), logo=channel_info.get('background'))
|
||||||
|
|
||||||
listing = [
|
listing = [
|
||||||
TitleItem(
|
TitleItem(
|
||||||
title=kodiutils.localize(30053, channel=channel.get('name')), # TV Guide for {channel}
|
title=kodiutils.localize(30053, channel=channel_info.get('name')), # TV Guide for {channel}
|
||||||
path=kodiutils.url_for('show_tvguide_channel', channel=key),
|
path=kodiutils.url_for('show_channel_tvguide', channel=channel),
|
||||||
art_dict={
|
art_dict={
|
||||||
'icon': 'DefaultAddonTvInfo.png',
|
'icon': 'DefaultAddonTvInfo.png',
|
||||||
'fanart': fanart,
|
'fanart': fanart,
|
||||||
},
|
},
|
||||||
info_dict={
|
info_dict={
|
||||||
'plot': kodiutils.localize(30054, channel=channel.get('name')), # Browse the TV Guide for {channel}
|
'plot': kodiutils.localize(30054, channel=channel_info.get('name')), # Browse the TV Guide for {channel}
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
TitleItem(
|
TitleItem(
|
||||||
title=kodiutils.localize(30055, channel=channel.get('name')), # Catalog for {channel}
|
title=kodiutils.localize(30055, channel=channel_info.get('name')), # Catalog for {channel}
|
||||||
path=kodiutils.url_for('show_catalog_channel', channel=key),
|
path=kodiutils.url_for('show_channel_catalog', channel=channel),
|
||||||
art_dict={
|
art_dict={
|
||||||
'icon': 'DefaultMovieTitle.png',
|
'icon': 'DefaultMovieTitle.png',
|
||||||
'fanart': fanart,
|
'fanart': fanart,
|
||||||
},
|
},
|
||||||
info_dict={
|
info_dict={
|
||||||
'plot': kodiutils.localize(30056, channel=channel.get('name')), # Browse the Catalog for {channel}
|
'plot': kodiutils.localize(30056, channel=channel_info.get('name')), # Browse the Catalog for {channel}
|
||||||
}
|
}
|
||||||
)
|
),
|
||||||
|
TitleItem(
|
||||||
|
title=kodiutils.localize(30057, channel=channel_info.get('name')), # Categories for {channel}
|
||||||
|
path=kodiutils.url_for('show_channel_categories', channel=channel),
|
||||||
|
art_dict={
|
||||||
|
'icon': 'DefaultGenre.png',
|
||||||
|
'fanart': fanart,
|
||||||
|
},
|
||||||
|
info_dict={
|
||||||
|
'plot': kodiutils.localize(30058, channel=channel_info.get('name')), # Browse the Categories for {channel}
|
||||||
|
}
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
# 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.get('youtube', []):
|
for youtube in channel_info.get('youtube', []):
|
||||||
listing.append(
|
listing.append(
|
||||||
TitleItem(
|
TitleItem(
|
||||||
title=kodiutils.localize(30206, label=youtube.get('label')), # Watch {label} on YouTube
|
title=kodiutils.localize(30206, label=youtube.get('label')), # Watch {label} on YouTube
|
||||||
@ -108,3 +124,58 @@ 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(channel, item.path, CACHE_ONLY) # Get program details, but from cache only
|
||||||
|
|
||||||
|
if program:
|
||||||
|
listing_programs.append(Menu.generate_titleitem(program))
|
||||||
|
else:
|
||||||
|
listing_programs.append(Menu.generate_titleitem(item))
|
||||||
|
|
||||||
|
# Add episodes
|
||||||
|
listing_episodes = []
|
||||||
|
for item in category.episodes:
|
||||||
|
# We don't have the Program Name without making a request to the page, so we use CACHE_AUTO instead of CACHE_ONLY.
|
||||||
|
# This will make a request for each item in this view (about 12 items), but it goes quite fast.
|
||||||
|
# Results are cached, so this will only happen once.
|
||||||
|
episode = self._api.get_episode(channel, item.path, CACHE_AUTO)
|
||||||
|
|
||||||
|
if episode:
|
||||||
|
listing_episodes.append(Menu.generate_titleitem(episode))
|
||||||
|
else:
|
||||||
|
listing_episodes.append(Menu.generate_titleitem(item))
|
||||||
|
|
||||||
|
kodiutils.show_listing(listing_programs + listing_episodes, 30007, content='tvshows', sort=['unsorted'])
|
||||||
|
@ -65,6 +65,7 @@ class Menu:
|
|||||||
art_dict = {
|
art_dict = {
|
||||||
'thumb': item.cover,
|
'thumb': item.cover,
|
||||||
'cover': item.cover,
|
'cover': item.cover,
|
||||||
|
'fanart': item.background or item.cover,
|
||||||
}
|
}
|
||||||
info_dict = {
|
info_dict = {
|
||||||
'title': item.title,
|
'title': item.title,
|
||||||
@ -78,9 +79,6 @@ class Menu:
|
|||||||
# Program
|
# Program
|
||||||
#
|
#
|
||||||
if isinstance(item, Program):
|
if isinstance(item, Program):
|
||||||
art_dict.update({
|
|
||||||
'fanart': item.background,
|
|
||||||
})
|
|
||||||
info_dict.update({
|
info_dict.update({
|
||||||
'mediatype': None,
|
'mediatype': None,
|
||||||
'season': len(item.seasons) if item.seasons else None,
|
'season': len(item.seasons) if item.seasons else None,
|
||||||
@ -102,9 +100,6 @@ class Menu:
|
|||||||
# Episode
|
# Episode
|
||||||
#
|
#
|
||||||
if isinstance(item, Episode):
|
if isinstance(item, Episode):
|
||||||
art_dict.update({
|
|
||||||
'fanart': item.cover,
|
|
||||||
})
|
|
||||||
info_dict.update({
|
info_dict.update({
|
||||||
'mediatype': 'episode',
|
'mediatype': 'episode',
|
||||||
'tvshowtitle': item.program_title,
|
'tvshowtitle': item.program_title,
|
||||||
@ -118,8 +113,21 @@ class Menu:
|
|||||||
'duration': item.duration,
|
'duration': item.duration,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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', channel=item.channel, page=quote(item.path, safe=''))
|
||||||
|
else:
|
||||||
|
# We have an UUID and can play this item directly
|
||||||
|
# This is not preferred since we will lack metadata
|
||||||
|
path = kodiutils.url_for('play', uuid=item.uuid)
|
||||||
|
|
||||||
return TitleItem(title=info_dict['title'],
|
return TitleItem(title=info_dict['title'],
|
||||||
path=kodiutils.url_for('play', uuid=item.uuid),
|
path=path,
|
||||||
art_dict=art_dict,
|
art_dict=art_dict,
|
||||||
info_dict=info_dict,
|
info_dict=info_dict,
|
||||||
stream_dict=stream_dict,
|
stream_dict=stream_dict,
|
||||||
|
@ -6,6 +6,7 @@ from __future__ import absolute_import, division, unicode_literals
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from resources.lib import kodiutils
|
from resources.lib import kodiutils
|
||||||
|
from resources.lib.modules.menu import Menu
|
||||||
from resources.lib.viervijfzes.auth import AuthApi
|
from resources.lib.viervijfzes.auth import AuthApi
|
||||||
from resources.lib.viervijfzes.auth_awsidp import InvalidLoginException, AuthenticationException
|
from resources.lib.viervijfzes.auth_awsidp import InvalidLoginException, AuthenticationException
|
||||||
from resources.lib.viervijfzes.content import ContentApi, UnavailableException, GeoblockedException
|
from resources.lib.viervijfzes.content import ContentApi, UnavailableException, GeoblockedException
|
||||||
@ -18,6 +19,11 @@ class Player:
|
|||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
""" Initialise object """
|
""" Initialise object """
|
||||||
|
auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password'), kodiutils.get_tokens_path())
|
||||||
|
self._api = ContentApi(auth, cache_path=kodiutils.get_cache_path())
|
||||||
|
|
||||||
|
# Workaround for Raspberry Pi 3 and older
|
||||||
|
kodiutils.set_global_setting('videoplayer.useomxplayer', True)
|
||||||
|
|
||||||
def play_from_page(self, channel, path):
|
def play_from_page(self, channel, path):
|
||||||
""" Play the requested item.
|
""" Play the requested item.
|
||||||
@ -25,22 +31,37 @@ class Player:
|
|||||||
:type path: string
|
:type path: string
|
||||||
"""
|
"""
|
||||||
# Get episode information
|
# Get episode information
|
||||||
episode = ContentApi().get_episode(channel, path)
|
episode = self._api.get_episode(channel, path)
|
||||||
|
resolved_stream = None
|
||||||
|
|
||||||
# Play this now we have the uuid
|
if episode.stream:
|
||||||
self.play(episode.uuid)
|
# We already have a resolved stream. Nice!
|
||||||
|
# We don't need credentials for these streams.
|
||||||
|
resolved_stream = episode.stream
|
||||||
|
_LOGGER.info('Already got a resolved stream: %s', resolved_stream)
|
||||||
|
|
||||||
|
if episode.uuid:
|
||||||
|
# Lookup the stream
|
||||||
|
resolved_stream = self._resolve_stream(episode.uuid)
|
||||||
|
_LOGGER.info('Resolved stream: %s', resolved_stream)
|
||||||
|
|
||||||
|
if resolved_stream:
|
||||||
|
titleitem = Menu.generate_titleitem(episode)
|
||||||
|
kodiutils.play(resolved_stream, info_dict=titleitem.info_dict, art_dict=titleitem.art_dict, prop_dict=titleitem.prop_dict)
|
||||||
|
|
||||||
|
def play(self, uuid):
|
||||||
|
""" Play the requested item.
|
||||||
|
:type uuid: string
|
||||||
|
"""
|
||||||
|
# Lookup the stream
|
||||||
|
resolved_stream = self._resolve_stream(uuid)
|
||||||
|
kodiutils.play(resolved_stream)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def play(item):
|
def _resolve_stream(uuid):
|
||||||
""" Play the requested item.
|
""" Resolve the stream for the requested item
|
||||||
:type item: string
|
:type uuid: string
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Workaround for Raspberry Pi 3 and older
|
|
||||||
omxplayer = kodiutils.get_global_setting('videoplayer.useomxplayer')
|
|
||||||
if omxplayer is False:
|
|
||||||
kodiutils.set_global_setting('videoplayer.useomxplayer', True)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Check if we have credentials
|
# Check if we have credentials
|
||||||
if not kodiutils.get_setting('username') or not kodiutils.get_setting('password'):
|
if not kodiutils.get_setting('username') or not kodiutils.get_setting('password'):
|
||||||
@ -49,28 +70,26 @@ class Player:
|
|||||||
if confirm:
|
if confirm:
|
||||||
kodiutils.open_settings()
|
kodiutils.open_settings()
|
||||||
kodiutils.end_of_directory()
|
kodiutils.end_of_directory()
|
||||||
return
|
return None
|
||||||
|
|
||||||
# Fetch an auth token now
|
# Fetch an auth token now
|
||||||
try:
|
try:
|
||||||
auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password'), kodiutils.get_tokens_path())
|
auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password'), kodiutils.get_tokens_path())
|
||||||
|
|
||||||
# Get stream information
|
# Get stream information
|
||||||
resolved_stream = ContentApi(auth).get_stream_by_uuid(item)
|
resolved_stream = ContentApi(auth).get_stream_by_uuid(uuid)
|
||||||
|
return resolved_stream
|
||||||
|
|
||||||
except (InvalidLoginException, AuthenticationException) as ex:
|
except (InvalidLoginException, AuthenticationException) as ex:
|
||||||
_LOGGER.error(ex)
|
_LOGGER.error(ex)
|
||||||
kodiutils.ok_dialog(message=kodiutils.localize(30702, error=str(ex)))
|
kodiutils.ok_dialog(message=kodiutils.localize(30702, error=str(ex)))
|
||||||
kodiutils.end_of_directory()
|
kodiutils.end_of_directory()
|
||||||
return
|
return None
|
||||||
|
|
||||||
except GeoblockedException:
|
except GeoblockedException:
|
||||||
kodiutils.ok_dialog(heading=kodiutils.localize(30709), message=kodiutils.localize(30710)) # This video is geo-blocked...
|
kodiutils.ok_dialog(heading=kodiutils.localize(30709), message=kodiutils.localize(30710)) # This video is geo-blocked...
|
||||||
return
|
return None
|
||||||
|
|
||||||
except UnavailableException:
|
except UnavailableException:
|
||||||
kodiutils.ok_dialog(heading=kodiutils.localize(30711), message=kodiutils.localize(30712)) # The video is unavailable...
|
kodiutils.ok_dialog(heading=kodiutils.localize(30711), message=kodiutils.localize(30712)) # The video is unavailable...
|
||||||
return
|
return None
|
||||||
|
|
||||||
# Play this item
|
|
||||||
kodiutils.play(resolved_stream)
|
|
||||||
|
@ -18,7 +18,6 @@ class Search:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
""" Initialise object """
|
""" Initialise object """
|
||||||
self._search = SearchApi()
|
self._search = SearchApi()
|
||||||
self._menu = Menu()
|
|
||||||
|
|
||||||
def show_search(self, query=None):
|
def show_search(self, query=None):
|
||||||
""" Shows the search dialog
|
""" Shows the search dialog
|
||||||
@ -40,7 +39,7 @@ class Search:
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Display results
|
# Display results
|
||||||
listing = [self._menu.generate_titleitem(item) for item in items]
|
listing = [Menu.generate_titleitem(item) for item in items]
|
||||||
|
|
||||||
# Sort like we get our results back.
|
# Sort like we get our results back.
|
||||||
kodiutils.show_listing(listing, 30009, content='tvshows')
|
kodiutils.show_listing(listing, 30009, content='tvshows')
|
||||||
|
@ -12,11 +12,6 @@ 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('tvguide')
|
_LOGGER = logging.getLogger('tvguide')
|
||||||
|
|
||||||
|
|
||||||
@ -70,7 +65,7 @@ class TvGuide:
|
|||||||
|
|
||||||
return dates
|
return dates
|
||||||
|
|
||||||
def show_tvguide_channel(self, channel):
|
def show_channel(self, channel):
|
||||||
""" Shows the dates in the tv guide
|
""" Shows the dates in the tv guide
|
||||||
:type channel: str
|
:type channel: str
|
||||||
"""
|
"""
|
||||||
@ -83,7 +78,7 @@ class TvGuide:
|
|||||||
|
|
||||||
listing.append(
|
listing.append(
|
||||||
TitleItem(title=title,
|
TitleItem(title=title,
|
||||||
path=kodiutils.url_for('show_tvguide_detail', channel=channel, date=day.get('key')),
|
path=kodiutils.url_for('show_channel_tvguide_detail', channel=channel, date=day.get('key')),
|
||||||
art_dict={
|
art_dict={
|
||||||
'icon': 'DefaultYear.png',
|
'icon': 'DefaultYear.png',
|
||||||
'thumb': 'DefaultYear.png',
|
'thumb': 'DefaultYear.png',
|
||||||
@ -96,7 +91,7 @@ class TvGuide:
|
|||||||
|
|
||||||
kodiutils.show_listing(listing, 30013, content='files', sort=['date'])
|
kodiutils.show_listing(listing, 30013, content='files', sort=['date'])
|
||||||
|
|
||||||
def show_tvguide_detail(self, channel=None, date=None):
|
def show_detail(self, channel=None, date=None):
|
||||||
""" Shows the programs of a specific date in the tv guide
|
""" Shows the programs of a specific date in the tv guide
|
||||||
:type channel: str
|
:type channel: str
|
||||||
:type date: str
|
:type date: str
|
||||||
@ -108,6 +103,11 @@ 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:
|
||||||
|
@ -37,7 +37,8 @@ class GeoblockedException(Exception):
|
|||||||
class Program:
|
class Program:
|
||||||
""" Defines a Program. """
|
""" Defines a Program. """
|
||||||
|
|
||||||
def __init__(self, uuid=None, path=None, channel=None, title=None, description=None, aired=None, cover=None, background=None, seasons=None, episodes=None):
|
def __init__(self, uuid=None, path=None, channel=None, title=None, description=None, aired=None, cover=None, background=None, seasons=None, episodes=None,
|
||||||
|
clips=None):
|
||||||
"""
|
"""
|
||||||
:type uuid: str
|
:type uuid: str
|
||||||
:type path: str
|
:type path: str
|
||||||
@ -49,6 +50,7 @@ class Program:
|
|||||||
:type background: str
|
:type background: str
|
||||||
:type seasons: list[Season]
|
:type seasons: list[Season]
|
||||||
:type episodes: list[Episode]
|
:type episodes: list[Episode]
|
||||||
|
:type clips: list[Episode]
|
||||||
"""
|
"""
|
||||||
self.uuid = uuid
|
self.uuid = uuid
|
||||||
self.path = path
|
self.path = path
|
||||||
@ -60,6 +62,7 @@ class Program:
|
|||||||
self.background = background
|
self.background = background
|
||||||
self.seasons = seasons
|
self.seasons = seasons
|
||||||
self.episodes = episodes
|
self.episodes = episodes
|
||||||
|
self.clips = clips
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "%r" % self.__dict__
|
return "%r" % self.__dict__
|
||||||
@ -94,8 +97,8 @@ class Season:
|
|||||||
class Episode:
|
class Episode:
|
||||||
""" Defines an Episode. """
|
""" Defines an Episode. """
|
||||||
|
|
||||||
def __init__(self, uuid=None, nodeid=None, path=None, channel=None, program_title=None, title=None, description=None, cover=None, duration=None,
|
def __init__(self, uuid=None, nodeid=None, path=None, channel=None, program_title=None, title=None, description=None, cover=None, background=None,
|
||||||
season=None, season_uuid=None, number=None, rating=None, aired=None, expiry=None):
|
duration=None, season=None, season_uuid=None, number=None, rating=None, aired=None, expiry=None, stream=None):
|
||||||
"""
|
"""
|
||||||
:type uuid: str
|
:type uuid: str
|
||||||
:type nodeid: str
|
:type nodeid: str
|
||||||
@ -105,6 +108,7 @@ class Episode:
|
|||||||
:type title: str
|
:type title: str
|
||||||
:type description: str
|
:type description: str
|
||||||
:type cover: str
|
:type cover: str
|
||||||
|
:type background: str
|
||||||
:type duration: int
|
:type duration: int
|
||||||
:type season: int
|
:type season: int
|
||||||
:type season_uuid: str
|
:type season_uuid: str
|
||||||
@ -112,6 +116,7 @@ class Episode:
|
|||||||
:type rating: str
|
:type rating: str
|
||||||
:type aired: datetime
|
:type aired: datetime
|
||||||
:type expiry: datetime
|
:type expiry: datetime
|
||||||
|
:type stream: string
|
||||||
"""
|
"""
|
||||||
self.uuid = uuid
|
self.uuid = uuid
|
||||||
self.nodeid = nodeid
|
self.nodeid = nodeid
|
||||||
@ -121,6 +126,7 @@ class Episode:
|
|||||||
self.title = title
|
self.title = title
|
||||||
self.description = description
|
self.description = description
|
||||||
self.cover = cover
|
self.cover = cover
|
||||||
|
self.background = background
|
||||||
self.duration = duration
|
self.duration = duration
|
||||||
self.season = season
|
self.season = season
|
||||||
self.season_uuid = season_uuid
|
self.season_uuid = season_uuid
|
||||||
@ -128,6 +134,28 @@ class Episode:
|
|||||||
self.rating = rating
|
self.rating = rating
|
||||||
self.aired = aired
|
self.aired = aired
|
||||||
self.expiry = expiry
|
self.expiry = expiry
|
||||||
|
self.stream = stream
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "%r" % self.__dict__
|
||||||
|
|
||||||
|
|
||||||
|
class Category:
|
||||||
|
""" Defines a Category. """
|
||||||
|
|
||||||
|
def __init__(self, uuid=None, channel=None, title=None, programs=None, episodes=None):
|
||||||
|
"""
|
||||||
|
:type uuid: str
|
||||||
|
:type channel: str
|
||||||
|
:type title: str
|
||||||
|
:type programs: List[Program]
|
||||||
|
:type episodes: List[Episode]
|
||||||
|
"""
|
||||||
|
self.uuid = uuid
|
||||||
|
self.channel = channel
|
||||||
|
self.title = title
|
||||||
|
self.programs = programs
|
||||||
|
self.episodes = episodes
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "%r" % self.__dict__
|
return "%r" % self.__dict__
|
||||||
@ -196,7 +224,7 @@ class ContentApi:
|
|||||||
title=title))
|
title=title))
|
||||||
return programs
|
return programs
|
||||||
|
|
||||||
def get_program(self, channel, path, cache=CACHE_AUTO):
|
def get_program(self, channel, path, extract_clips=False, cache=CACHE_AUTO):
|
||||||
""" Get a Program object from the specified page.
|
""" Get a Program object from the specified page.
|
||||||
:type channel: str
|
:type channel: str
|
||||||
:type path: str
|
:type path: str
|
||||||
@ -206,11 +234,18 @@ class ContentApi:
|
|||||||
if channel not in CHANNELS:
|
if channel not in CHANNELS:
|
||||||
raise Exception('Unknown channel %s' % channel)
|
raise Exception('Unknown channel %s' % channel)
|
||||||
|
|
||||||
|
# We want to use the html to extract clips
|
||||||
|
# This is the worst hack, since Python 2.7 doesn't support nonlocal
|
||||||
|
raw_html = [None]
|
||||||
|
|
||||||
def update():
|
def update():
|
||||||
""" Fetch the program metadata by scraping """
|
""" Fetch the program metadata by scraping """
|
||||||
# Fetch webpage
|
# Fetch webpage
|
||||||
page = self._get_url(CHANNELS[channel]['url'] + '/' + path)
|
page = self._get_url(CHANNELS[channel]['url'] + '/' + path)
|
||||||
|
|
||||||
|
# Store a copy in the parent's raw_html var.
|
||||||
|
raw_html[0] = page
|
||||||
|
|
||||||
# Extract JSON
|
# Extract JSON
|
||||||
regex_program = re.compile(r'data-hero="([^"]+)', re.DOTALL)
|
regex_program = re.compile(r'data-hero="([^"]+)', re.DOTALL)
|
||||||
json_data = HTMLParser().unescape(regex_program.search(page).group(1))
|
json_data = HTMLParser().unescape(regex_program.search(page).group(1))
|
||||||
@ -220,40 +255,79 @@ class ContentApi:
|
|||||||
|
|
||||||
# Fetch listing from cache or update if needed
|
# Fetch listing from cache or update if needed
|
||||||
data = self._handle_cache(key=['program', channel, path], cache_mode=cache, update=update)
|
data = self._handle_cache(key=['program', channel, path], cache_mode=cache, update=update)
|
||||||
|
if not data:
|
||||||
|
return None
|
||||||
|
|
||||||
program = self._parse_program_data(data)
|
program = self._parse_program_data(data)
|
||||||
|
|
||||||
|
# Also extract clips if we did a real HTTP call
|
||||||
|
if extract_clips and raw_html[0]:
|
||||||
|
clips = self._extract_videos(raw_html[0], channel)
|
||||||
|
program.clips = clips
|
||||||
|
|
||||||
return program
|
return program
|
||||||
|
|
||||||
def get_episode(self, channel, path):
|
def get_episode(self, channel, path, cache=CACHE_AUTO):
|
||||||
""" Get a Episode object from the specified page.
|
""" Get a Episode object from the specified page.
|
||||||
:type channel: str
|
:type channel: str
|
||||||
:type path: str
|
:type path: str
|
||||||
|
:type cache: str
|
||||||
:rtype Episode
|
:rtype Episode
|
||||||
NOTE: This function doesn't use an API.
|
|
||||||
"""
|
"""
|
||||||
if channel not in CHANNELS:
|
if channel not in CHANNELS:
|
||||||
raise Exception('Unknown channel %s' % channel)
|
raise Exception('Unknown channel %s' % channel)
|
||||||
|
|
||||||
|
def update():
|
||||||
|
""" Fetch the program metadata by scraping """
|
||||||
# Load webpage
|
# Load webpage
|
||||||
page = self._get_url(CHANNELS[channel]['url'] + '/' + path)
|
page = self._get_url(CHANNELS[channel]['url'] + '/' + path)
|
||||||
|
|
||||||
# Extract program JSON
|
|
||||||
parser = HTMLParser()
|
parser = HTMLParser()
|
||||||
|
program_json = None
|
||||||
|
episode_json = None
|
||||||
|
|
||||||
|
# Extract video JSON by looking for a data-video tag
|
||||||
|
# This is not present on every page
|
||||||
|
regex_video_data = re.compile(r'data-video="([^"]+)"', re.DOTALL)
|
||||||
|
result = regex_video_data.search(page)
|
||||||
|
if result:
|
||||||
|
video_id = json.loads(parser.unescape(result.group(1)))['id']
|
||||||
|
video_json_data = self._get_url('%s/video/%s' % (self.SITE_APIS[channel], video_id))
|
||||||
|
video_json = json.loads(video_json_data)
|
||||||
|
return dict(video=video_json)
|
||||||
|
|
||||||
|
# Extract program JSON
|
||||||
regex_program = re.compile(r'data-hero="([^"]+)', re.DOTALL)
|
regex_program = re.compile(r'data-hero="([^"]+)', re.DOTALL)
|
||||||
json_data = parser.unescape(regex_program.search(page).group(1))
|
result = regex_program.search(page)
|
||||||
data = json.loads(json_data)['data']
|
if result:
|
||||||
program = self._parse_program_data(data)
|
program_json_data = parser.unescape(result.group(1))
|
||||||
|
program_json = json.loads(program_json_data)['data']
|
||||||
|
|
||||||
# Extract episode JSON
|
# Extract episode JSON
|
||||||
regex_episode = re.compile(r'<script type="application/json" data-drupal-selector="drupal-settings-json">(.*?)</script>', re.DOTALL)
|
regex_episode = re.compile(r'<script type="application/json" data-drupal-selector="drupal-settings-json">(.*?)</script>', re.DOTALL)
|
||||||
json_data = parser.unescape(regex_episode.search(page).group(1))
|
result = regex_episode.search(page)
|
||||||
data = json.loads(json_data)
|
if result:
|
||||||
|
episode_json_data = parser.unescape(result.group(1))
|
||||||
|
episode_json = json.loads(episode_json_data)
|
||||||
|
|
||||||
# Lookup the episode in the program JSON based on the nodeId
|
return dict(program=program_json, episode=episode_json)
|
||||||
# The episode we just found doesn't contain all information
|
|
||||||
|
# Fetch listing from cache or update if needed
|
||||||
|
data = self._handle_cache(key=['episode', channel, path], cache_mode=cache, update=update)
|
||||||
|
if not data:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if 'video' in data and data['video']:
|
||||||
|
# We have found detailed episode information
|
||||||
|
episode = self._parse_episode_data(data['video'])
|
||||||
|
return episode
|
||||||
|
|
||||||
|
if 'program' in data and 'episode' in data and data['program'] and data['episode']:
|
||||||
|
# We don't have detailed episode information
|
||||||
|
# We need to lookup the episode in the program JSON
|
||||||
|
program = self._parse_program_data(data['program'])
|
||||||
for episode in program.episodes:
|
for episode in program.episodes:
|
||||||
if episode.nodeid == data['pageInfo']['nodeId']:
|
if episode.nodeid == data['episode']['pageInfo']['nodeId']:
|
||||||
return episode
|
return episode
|
||||||
|
|
||||||
return None
|
return None
|
||||||
@ -267,15 +341,146 @@ class ContentApi:
|
|||||||
data = json.loads(response)
|
data = json.loads(response)
|
||||||
return data['video']['S']
|
return data['video']['S']
|
||||||
|
|
||||||
|
def get_categories(self, channel):
|
||||||
|
""" Get a list of all categories of the specified channel.
|
||||||
|
:type channel: str
|
||||||
|
:rtype list[Category]
|
||||||
|
"""
|
||||||
|
if channel not in CHANNELS:
|
||||||
|
raise Exception('Unknown channel %s' % channel)
|
||||||
|
|
||||||
|
# Load webpage
|
||||||
|
raw_html = self._get_url(CHANNELS[channel]['url'])
|
||||||
|
|
||||||
|
# Categories regexes
|
||||||
|
regex_articles = re.compile(r'<article([^>]+)>(.*?)</article>', re.DOTALL)
|
||||||
|
regex_submenu_id = re.compile(r'data-submenu-id="([^"]*)"') # splitted since the order might change
|
||||||
|
regex_submenu_title = re.compile(r'data-submenu-title="([^"]*)"')
|
||||||
|
|
||||||
|
categories = []
|
||||||
|
for result in regex_articles.finditer(raw_html):
|
||||||
|
article_info_html = result.group(1)
|
||||||
|
article_html = result.group(2)
|
||||||
|
category_title = regex_submenu_title.search(article_info_html).group(1)
|
||||||
|
category_id = regex_submenu_id.search(article_info_html).group(1)
|
||||||
|
|
||||||
|
# Skip empty categories or 'All programs'
|
||||||
|
if not category_id or category_id == 'programmas':
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Extract items
|
||||||
|
programs = self._extract_programs(article_html, channel)
|
||||||
|
episodes = self._extract_videos(article_html, channel)
|
||||||
|
categories.append(Category(uuid=category_id, channel=channel, title=category_title, programs=programs, episodes=episodes))
|
||||||
|
|
||||||
|
return categories
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _extract_programs(html, channel):
|
||||||
|
""" Extract Programs from HTML code """
|
||||||
|
parser = HTMLParser()
|
||||||
|
|
||||||
|
# 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 = parser.unescape(item.group('title'))
|
||||||
|
|
||||||
|
# Program
|
||||||
|
programs.append(Program(
|
||||||
|
path=path.lstrip('/'),
|
||||||
|
channel=channel,
|
||||||
|
title=title,
|
||||||
|
))
|
||||||
|
|
||||||
|
return programs
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _extract_videos(html, channel):
|
||||||
|
""" Extract videos from HTML code """
|
||||||
|
parser = HTMLParser()
|
||||||
|
|
||||||
|
# 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_duration = re.compile(r'data-duration="([^"]*)"')
|
||||||
|
regex_episode_video_id = re.compile(r'data-videoid="([^"]*)"')
|
||||||
|
regex_episode_image = re.compile(r'data-background-image="([^"]*)"')
|
||||||
|
regex_episode_timestamp = re.compile(r'data-timestamp="([^"]*)"')
|
||||||
|
|
||||||
|
# Extract items
|
||||||
|
episodes = []
|
||||||
|
for item in regex_item.finditer(html):
|
||||||
|
item_html = item.group(0)
|
||||||
|
path = item.group('path')
|
||||||
|
|
||||||
|
# Extract title
|
||||||
|
try:
|
||||||
|
title = parser.unescape(regex_episode_title.search(item_html).group(1))
|
||||||
|
except AttributeError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# This is not a episode
|
||||||
|
if not path.startswith('/video'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
episode_program = regex_episode_program.search(item_html).group(1)
|
||||||
|
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 = parser.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))
|
||||||
|
except AttributeError:
|
||||||
|
_LOGGER.warning('Found no episode_timestamp for %s', title)
|
||||||
|
episode_timestamp = None
|
||||||
|
|
||||||
|
# Episode
|
||||||
|
episodes.append(Episode(
|
||||||
|
path=path.lstrip('/'),
|
||||||
|
channel=channel,
|
||||||
|
title=title,
|
||||||
|
duration=episode_duration,
|
||||||
|
uuid=episode_video_id,
|
||||||
|
aired=datetime.fromtimestamp(episode_timestamp) if episode_timestamp else None,
|
||||||
|
cover=episode_image,
|
||||||
|
program_title=episode_program,
|
||||||
|
))
|
||||||
|
|
||||||
|
return episodes
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _parse_program_data(data):
|
def _parse_program_data(data):
|
||||||
""" Parse the Program JSON.
|
""" Parse the Program JSON.
|
||||||
:type data: dict
|
:type data: dict
|
||||||
:rtype Program
|
:rtype Program
|
||||||
"""
|
"""
|
||||||
if data is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Create Program info
|
# Create Program info
|
||||||
program = Program(
|
program = Program(
|
||||||
uuid=data['id'],
|
uuid=data['id'],
|
||||||
@ -311,7 +516,7 @@ class ContentApi:
|
|||||||
return program
|
return program
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _parse_episode_data(data, season_uuid):
|
def _parse_episode_data(data, season_uuid=None):
|
||||||
""" Parse the Episode JSON.
|
""" Parse the Episode JSON.
|
||||||
:type data: dict
|
:type data: dict
|
||||||
:type season_uuid: str
|
:type season_uuid: str
|
||||||
@ -337,13 +542,15 @@ class ContentApi:
|
|||||||
title=data.get('title'),
|
title=data.get('title'),
|
||||||
description=data.get('pageInfo', {}).get('description'),
|
description=data.get('pageInfo', {}).get('description'),
|
||||||
cover=data.get('image'),
|
cover=data.get('image'),
|
||||||
|
background=data.get('image'),
|
||||||
duration=data.get('duration'),
|
duration=data.get('duration'),
|
||||||
season=data.get('seasonNumber'),
|
season=data.get('seasonNumber'),
|
||||||
season_uuid=season_uuid,
|
season_uuid=season_uuid,
|
||||||
number=episode_number,
|
number=episode_number,
|
||||||
aired=datetime.fromtimestamp(data.get('createdDate')),
|
aired=datetime.fromtimestamp(data.get('createdDate')),
|
||||||
expiry=datetime.fromtimestamp(data.get('unpublishDate')) if data.get('unpublishDate') else None,
|
expiry=datetime.fromtimestamp(data.get('unpublishDate')) if data.get('unpublishDate') else None,
|
||||||
rating=data.get('parentalRating')
|
rating=data.get('parentalRating'),
|
||||||
|
stream=data.get('path'),
|
||||||
)
|
)
|
||||||
return episode
|
return episode
|
||||||
|
|
||||||
@ -393,7 +600,7 @@ class ContentApi:
|
|||||||
|
|
||||||
def _get_cache(self, key, allow_expired=False):
|
def _get_cache(self, key, allow_expired=False):
|
||||||
""" Get an item from the cache """
|
""" Get an item from the cache """
|
||||||
filename = '.'.join(key) + '.json'
|
filename = ('.'.join(key) + '.json').replace('/', '_')
|
||||||
fullpath = self._cache_path + filename
|
fullpath = self._cache_path + filename
|
||||||
|
|
||||||
if not os.path.exists(fullpath):
|
if not os.path.exists(fullpath):
|
||||||
@ -412,7 +619,7 @@ class ContentApi:
|
|||||||
|
|
||||||
def _set_cache(self, key, data, ttl):
|
def _set_cache(self, key, data, ttl):
|
||||||
""" Store an item in the cache """
|
""" Store an item in the cache """
|
||||||
filename = '.'.join(key) + '.json'
|
filename = ('.'.join(key) + '.json').replace('/', '_')
|
||||||
fullpath = self._cache_path + filename
|
fullpath = self._cache_path + filename
|
||||||
|
|
||||||
if not os.path.exists(self._cache_path):
|
if not os.path.exists(self._cache_path):
|
||||||
|
@ -10,7 +10,7 @@ import unittest
|
|||||||
|
|
||||||
import resources.lib.kodiutils as kodiutils
|
import resources.lib.kodiutils as kodiutils
|
||||||
from resources.lib.viervijfzes.auth import AuthApi
|
from resources.lib.viervijfzes.auth import AuthApi
|
||||||
from resources.lib.viervijfzes.content import ContentApi, Program, Episode
|
from resources.lib.viervijfzes.content import ContentApi, Program, Episode, Category, CACHE_PREVENT
|
||||||
|
|
||||||
_LOGGER = logging.getLogger('test-api')
|
_LOGGER = logging.getLogger('test-api')
|
||||||
|
|
||||||
@ -27,14 +27,31 @@ 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):
|
||||||
|
for channel in ['vier', 'vijf', 'zes']:
|
||||||
|
categories = self._api.get_categories(channel)
|
||||||
|
self.assertIsInstance(categories, list)
|
||||||
|
if categories:
|
||||||
|
self.assertIsInstance(categories[0], Category)
|
||||||
|
|
||||||
def test_episodes(self):
|
def test_episodes(self):
|
||||||
for channel, program in [('vier', 'auwch'), ('vijf', 'zo-man-zo-vrouw')]:
|
for channel, program in [('vier', 'auwch'), ('vijf', 'zo-man-zo-vrouw')]:
|
||||||
program = self._api.get_program(channel, program)
|
program = self._api.get_program(channel, program, cache=CACHE_PREVENT)
|
||||||
self.assertIsInstance(program, Program)
|
self.assertIsInstance(program, Program)
|
||||||
self.assertIsInstance(program.seasons, dict)
|
self.assertIsInstance(program.seasons, dict)
|
||||||
self.assertIsInstance(program.episodes, list)
|
self.assertIsInstance(program.episodes, list)
|
||||||
self.assertIsInstance(program.episodes[0], Episode)
|
self.assertIsInstance(program.episodes[0], Episode)
|
||||||
|
|
||||||
|
def test_clips(self):
|
||||||
|
for channel, program in [('vier', 'gert-late-night'), ('zes', 'macgyver')]:
|
||||||
|
program = self._api.get_program(channel, program, extract_clips=True, cache=CACHE_PREVENT)
|
||||||
|
|
||||||
|
self.assertIsInstance(program.clips, list)
|
||||||
|
self.assertIsInstance(program.clips[0], Episode)
|
||||||
|
|
||||||
|
episode = self._api.get_episode(channel, program.clips[0].path, cache=CACHE_PREVENT)
|
||||||
|
self.assertIsInstance(episode, Episode)
|
||||||
|
|
||||||
@unittest.skipUnless(kodiutils.get_setting('username') and kodiutils.get_setting('password'), 'Skipping since we have no credentials.')
|
@unittest.skipUnless(kodiutils.get_setting('username') and kodiutils.get_setting('password'), 'Skipping since we have no credentials.')
|
||||||
def test_get_stream(self):
|
def test_get_stream(self):
|
||||||
program = self._api.get_program('vier', 'auwch')
|
program = self._api.get_program('vier', 'auwch')
|
||||||
|
@ -40,7 +40,7 @@ class TestRouting(unittest.TestCase):
|
|||||||
routing.run([routing.url_for(addon.show_catalog), '0', ''])
|
routing.run([routing.url_for(addon.show_catalog), '0', ''])
|
||||||
|
|
||||||
def test_catalog_channel_menu(self):
|
def test_catalog_channel_menu(self):
|
||||||
routing.run([routing.url_for(addon.show_catalog_channel, channel='vier'), '0', ''])
|
routing.run([routing.url_for(addon.show_channel_catalog, channel='vier'), '0', ''])
|
||||||
|
|
||||||
def test_catalog_program_menu(self):
|
def test_catalog_program_menu(self):
|
||||||
routing.run([routing.url_for(addon.show_catalog_program, channel='vier', program='de-mol'), '0', ''])
|
routing.run([routing.url_for(addon.show_catalog_program, channel='vier', program='de-mol'), '0', ''])
|
||||||
@ -53,8 +53,8 @@ class TestRouting(unittest.TestCase):
|
|||||||
routing.run([routing.url_for(addon.show_search, query='de mol'), '0', ''])
|
routing.run([routing.url_for(addon.show_search, query='de mol'), '0', ''])
|
||||||
|
|
||||||
def test_tvguide_menu(self):
|
def test_tvguide_menu(self):
|
||||||
routing.run([routing.url_for(addon.show_tvguide_channel, channel='vier'), '0', ''])
|
routing.run([routing.url_for(addon.show_channel_tvguide, channel='vier'), '0', ''])
|
||||||
routing.run([routing.url_for(addon.show_tvguide_detail, channel='vier', date='today'), '0', ''])
|
routing.run([routing.url_for(addon.show_channel_tvguide_detail, channel='vier', date='today'), '0', ''])
|
||||||
|
|
||||||
def test_metadata_update(self):
|
def test_metadata_update(self):
|
||||||
routing.run([routing.url_for(addon.metadata_update), '0', ''])
|
routing.run([routing.url_for(addon.metadata_update), '0', ''])
|
||||||
|
Loading…
Reference in New Issue
Block a user