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]"
|
||||
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
|
||||
msgctxt "#30102"
|
||||
|
@ -65,6 +65,22 @@ msgctxt "#30056"
|
||||
msgid "Browse the Catalog for [B]{channel}[/B]"
|
||||
msgstr "Doorblader de catalogus voor [B]{channel}[/B]"
|
||||
|
||||
msgctxt "#30057"
|
||||
msgid "Categories for [B]{channel}[/B]"
|
||||
msgstr "Categoriën voor [B]{channel}[/B]"
|
||||
|
||||
msgctxt "#30058"
|
||||
msgid "Browse the Categories for [B]{channel}[/B]"
|
||||
msgstr "Doorblader de categoriën van [B]{channel}[/B]"
|
||||
|
||||
msgctxt "#30059"
|
||||
msgid "Clips of [B]{program}[/B]"
|
||||
msgstr "Clips van [B]{program}[/B]"
|
||||
|
||||
msgctxt "#30060"
|
||||
msgid "Watch short clips of [B]{program}[/B]"
|
||||
msgstr "Bekijk korte videoclips van [B]{program}[/B]"
|
||||
|
||||
|
||||
### CONTEXT MENU
|
||||
msgctxt "#30102"
|
||||
|
@ -35,18 +35,32 @@ def show_channel_menu(channel):
|
||||
Channels().show_channel_menu(channel)
|
||||
|
||||
|
||||
@routing.route('/tvguide/channel/<channel>')
|
||||
def show_tvguide_channel(channel):
|
||||
@routing.route('/channels/<channel>/categories')
|
||||
def show_channel_categories(channel):
|
||||
""" Shows TV Channel categories """
|
||||
from resources.lib.modules.channels import Channels
|
||||
Channels().show_channel_categories(channel)
|
||||
|
||||
|
||||
@routing.route('/channels/<channel>/categories/<category>')
|
||||
def show_channel_category(channel, category):
|
||||
""" Shows TV Channel categories """
|
||||
from resources.lib.modules.channels import Channels
|
||||
Channels().show_channel_category(channel, category)
|
||||
|
||||
|
||||
@routing.route('/channels/<channel>/tvguide')
|
||||
def show_channel_tvguide(channel):
|
||||
""" Shows the dates in the tv guide """
|
||||
from resources.lib.modules.tvguide import TvGuide
|
||||
TvGuide().show_tvguide_channel(channel)
|
||||
TvGuide().show_channel(channel)
|
||||
|
||||
|
||||
@routing.route('/tvguide/channel/<channel>/<date>')
|
||||
def show_tvguide_detail(channel=None, date=None):
|
||||
@routing.route('/channels/<channel>/tvguide/<date>')
|
||||
def show_channel_tvguide_detail(channel=None, date=None):
|
||||
""" Shows the programs of a specific date in the tv guide """
|
||||
from resources.lib.modules.tvguide import TvGuide
|
||||
TvGuide().show_tvguide_detail(channel, date)
|
||||
TvGuide().show_detail(channel, date)
|
||||
|
||||
|
||||
@routing.route('/catalog')
|
||||
@ -56,23 +70,30 @@ def show_catalog():
|
||||
Catalog().show_catalog()
|
||||
|
||||
|
||||
@routing.route('/catalog/by-channel/<channel>')
|
||||
def show_catalog_channel(channel):
|
||||
@routing.route('/catalog/<channel>')
|
||||
def show_channel_catalog(channel):
|
||||
""" Show a category in the catalog """
|
||||
from resources.lib.modules.catalog import Catalog
|
||||
Catalog().show_catalog_channel(channel)
|
||||
|
||||
|
||||
@routing.route('/catalog/program/<channel>/<program>')
|
||||
@routing.route('/catalog/<channel>/<program>')
|
||||
def show_catalog_program(channel, program):
|
||||
""" Show a program from the catalog """
|
||||
from resources.lib.modules.catalog import Catalog
|
||||
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):
|
||||
""" Show a program from the catalog """
|
||||
""" Show a season from a program """
|
||||
from resources.lib.modules.catalog import Catalog
|
||||
Catalog().show_program_season(channel, program, season)
|
||||
|
||||
|
@ -22,7 +22,6 @@ class Catalog:
|
||||
""" 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())
|
||||
self._menu = Menu()
|
||||
|
||||
def show_catalog(self):
|
||||
""" Show all the programs of all channels """
|
||||
@ -34,7 +33,7 @@ class Catalog:
|
||||
kodiutils.notification(message=str(ex))
|
||||
raise
|
||||
|
||||
listing = [self._menu.generate_titleitem(item) for item in items]
|
||||
listing = [Menu.generate_titleitem(item) for item in items]
|
||||
|
||||
# Sort items by title
|
||||
# Used for A-Z listing or when movies and episodes are mixed.
|
||||
@ -52,7 +51,7 @@ class Catalog:
|
||||
|
||||
listing = []
|
||||
for item in items:
|
||||
listing.append(self._menu.generate_titleitem(item))
|
||||
listing.append(Menu.generate_titleitem(item))
|
||||
|
||||
# Sort items by title
|
||||
# Used for A-Z listing or when movies and episodes are mixed.
|
||||
@ -64,19 +63,19 @@ class Catalog:
|
||||
:type program_id: str
|
||||
"""
|
||||
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:
|
||||
kodiutils.ok_dialog(message=kodiutils.localize(30717)) # This program is not available in the catalogue.
|
||||
kodiutils.end_of_directory()
|
||||
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.end_of_directory()
|
||||
return
|
||||
|
||||
# Go directly to the season when we have only one season
|
||||
if len(program.seasons) == 1:
|
||||
# Go directly to the season when we have only one season and no clips
|
||||
if not program.clips and len(program.seasons) == 1:
|
||||
self.show_program_season(channel, program_id, list(program.seasons.values())[0].uuid)
|
||||
return
|
||||
|
||||
@ -85,7 +84,7 @@ class Catalog:
|
||||
listing = []
|
||||
|
||||
# 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(
|
||||
TitleItem(
|
||||
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.
|
||||
kodiutils.show_listing(listing, 30003, content='tvshows')
|
||||
|
||||
@ -145,7 +163,25 @@ class Catalog:
|
||||
# Show the episodes of the season that was selected
|
||||
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.
|
||||
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.kodiutils import TitleItem
|
||||
from resources.lib.modules.menu import Menu
|
||||
from resources.lib.viervijfzes import CHANNELS, STREAM_DICT
|
||||
from resources.lib.viervijfzes.auth import AuthApi
|
||||
from resources.lib.viervijfzes.content import ContentApi, CACHE_ONLY, CACHE_AUTO
|
||||
|
||||
_LOGGER = logging.getLogger('channels')
|
||||
|
||||
@ -17,6 +20,8 @@ class Channels:
|
||||
|
||||
def __init__(self):
|
||||
""" 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
|
||||
def show_channels():
|
||||
@ -33,7 +38,7 @@ class Channels:
|
||||
(
|
||||
kodiutils.localize(30053, channel=channel.get('name')), # TV Guide for {channel}
|
||||
'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)
|
||||
|
||||
@staticmethod
|
||||
def show_channel_menu(key):
|
||||
def show_channel_menu(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
|
||||
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 = [
|
||||
TitleItem(
|
||||
title=kodiutils.localize(30053, channel=channel.get('name')), # TV Guide for {channel}
|
||||
path=kodiutils.url_for('show_tvguide_channel', channel=key),
|
||||
title=kodiutils.localize(30053, channel=channel_info.get('name')), # TV Guide for {channel}
|
||||
path=kodiutils.url_for('show_channel_tvguide', channel=channel),
|
||||
art_dict={
|
||||
'icon': 'DefaultAddonTvInfo.png',
|
||||
'fanart': fanart,
|
||||
},
|
||||
info_dict={
|
||||
'plot': kodiutils.localize(30054, channel=channel.get('name')), # Browse the TV Guide for {channel}
|
||||
'plot': kodiutils.localize(30054, channel=channel_info.get('name')), # Browse the TV Guide for {channel}
|
||||
}
|
||||
),
|
||||
TitleItem(
|
||||
title=kodiutils.localize(30055, channel=channel.get('name')), # Catalog for {channel}
|
||||
path=kodiutils.url_for('show_catalog_channel', channel=key),
|
||||
title=kodiutils.localize(30055, channel=channel_info.get('name')), # Catalog for {channel}
|
||||
path=kodiutils.url_for('show_channel_catalog', channel=channel),
|
||||
art_dict={
|
||||
'icon': 'DefaultMovieTitle.png',
|
||||
'fanart': fanart,
|
||||
},
|
||||
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
|
||||
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(
|
||||
TitleItem(
|
||||
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'])
|
||||
|
||||
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 = {
|
||||
'thumb': item.cover,
|
||||
'cover': item.cover,
|
||||
'fanart': item.background or item.cover,
|
||||
}
|
||||
info_dict = {
|
||||
'title': item.title,
|
||||
@ -78,9 +79,6 @@ class Menu:
|
||||
# Program
|
||||
#
|
||||
if isinstance(item, Program):
|
||||
art_dict.update({
|
||||
'fanart': item.background,
|
||||
})
|
||||
info_dict.update({
|
||||
'mediatype': None,
|
||||
'season': len(item.seasons) if item.seasons else None,
|
||||
@ -102,9 +100,6 @@ class Menu:
|
||||
# Episode
|
||||
#
|
||||
if isinstance(item, Episode):
|
||||
art_dict.update({
|
||||
'fanart': item.cover,
|
||||
})
|
||||
info_dict.update({
|
||||
'mediatype': 'episode',
|
||||
'tvshowtitle': item.program_title,
|
||||
@ -118,8 +113,21 @@ class Menu:
|
||||
'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'],
|
||||
path=kodiutils.url_for('play', uuid=item.uuid),
|
||||
path=path,
|
||||
art_dict=art_dict,
|
||||
info_dict=info_dict,
|
||||
stream_dict=stream_dict,
|
||||
|
@ -6,6 +6,7 @@ from __future__ import absolute_import, division, unicode_literals
|
||||
import logging
|
||||
|
||||
from resources.lib import kodiutils
|
||||
from resources.lib.modules.menu import Menu
|
||||
from resources.lib.viervijfzes.auth import AuthApi
|
||||
from resources.lib.viervijfzes.auth_awsidp import InvalidLoginException, AuthenticationException
|
||||
from resources.lib.viervijfzes.content import ContentApi, UnavailableException, GeoblockedException
|
||||
@ -18,6 +19,11 @@ class Player:
|
||||
|
||||
def __init__(self):
|
||||
""" 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):
|
||||
""" Play the requested item.
|
||||
@ -25,22 +31,37 @@ class Player:
|
||||
:type path: string
|
||||
"""
|
||||
# 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
|
||||
self.play(episode.uuid)
|
||||
if episode.stream:
|
||||
# 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
|
||||
def play(item):
|
||||
""" Play the requested item.
|
||||
:type item: string
|
||||
def _resolve_stream(uuid):
|
||||
""" Resolve the stream for the requested item
|
||||
: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:
|
||||
# Check if we have credentials
|
||||
if not kodiutils.get_setting('username') or not kodiutils.get_setting('password'):
|
||||
@ -49,28 +70,26 @@ class Player:
|
||||
if confirm:
|
||||
kodiutils.open_settings()
|
||||
kodiutils.end_of_directory()
|
||||
return
|
||||
return None
|
||||
|
||||
# Fetch an auth token now
|
||||
try:
|
||||
auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password'), kodiutils.get_tokens_path())
|
||||
|
||||
# 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:
|
||||
_LOGGER.error(ex)
|
||||
kodiutils.ok_dialog(message=kodiutils.localize(30702, error=str(ex)))
|
||||
kodiutils.end_of_directory()
|
||||
return
|
||||
return None
|
||||
|
||||
except GeoblockedException:
|
||||
kodiutils.ok_dialog(heading=kodiutils.localize(30709), message=kodiutils.localize(30710)) # This video is geo-blocked...
|
||||
return
|
||||
return None
|
||||
|
||||
except UnavailableException:
|
||||
kodiutils.ok_dialog(heading=kodiutils.localize(30711), message=kodiutils.localize(30712)) # The video is unavailable...
|
||||
return
|
||||
|
||||
# Play this item
|
||||
kodiutils.play(resolved_stream)
|
||||
return None
|
||||
|
@ -18,7 +18,6 @@ class Search:
|
||||
def __init__(self):
|
||||
""" Initialise object """
|
||||
self._search = SearchApi()
|
||||
self._menu = Menu()
|
||||
|
||||
def show_search(self, query=None):
|
||||
""" Shows the search dialog
|
||||
@ -40,7 +39,7 @@ class Search:
|
||||
return
|
||||
|
||||
# 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.
|
||||
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.epg import EpgApi
|
||||
|
||||
try: # Python 3
|
||||
from urllib.parse import quote
|
||||
except ImportError: # Python 2
|
||||
from urllib import quote
|
||||
|
||||
_LOGGER = logging.getLogger('tvguide')
|
||||
|
||||
|
||||
@ -70,7 +65,7 @@ class TvGuide:
|
||||
|
||||
return dates
|
||||
|
||||
def show_tvguide_channel(self, channel):
|
||||
def show_channel(self, channel):
|
||||
""" Shows the dates in the tv guide
|
||||
:type channel: str
|
||||
"""
|
||||
@ -83,7 +78,7 @@ class TvGuide:
|
||||
|
||||
listing.append(
|
||||
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={
|
||||
'icon': 'DefaultYear.png',
|
||||
'thumb': 'DefaultYear.png',
|
||||
@ -96,7 +91,7 @@ class TvGuide:
|
||||
|
||||
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
|
||||
:type channel: str
|
||||
:type date: str
|
||||
@ -108,6 +103,11 @@ class TvGuide:
|
||||
kodiutils.end_of_directory()
|
||||
return
|
||||
|
||||
try: # Python 3
|
||||
from urllib.parse import quote
|
||||
except ImportError: # Python 2
|
||||
from urllib import quote
|
||||
|
||||
listing = []
|
||||
for program in programs:
|
||||
if program.program_url:
|
||||
|
@ -37,7 +37,8 @@ class GeoblockedException(Exception):
|
||||
class 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 path: str
|
||||
@ -49,6 +50,7 @@ class Program:
|
||||
:type background: str
|
||||
:type seasons: list[Season]
|
||||
:type episodes: list[Episode]
|
||||
:type clips: list[Episode]
|
||||
"""
|
||||
self.uuid = uuid
|
||||
self.path = path
|
||||
@ -60,6 +62,7 @@ class Program:
|
||||
self.background = background
|
||||
self.seasons = seasons
|
||||
self.episodes = episodes
|
||||
self.clips = clips
|
||||
|
||||
def __repr__(self):
|
||||
return "%r" % self.__dict__
|
||||
@ -94,8 +97,8 @@ class Season:
|
||||
class 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,
|
||||
season=None, season_uuid=None, number=None, rating=None, aired=None, expiry=None):
|
||||
def __init__(self, uuid=None, nodeid=None, path=None, channel=None, program_title=None, title=None, description=None, cover=None, background=None,
|
||||
duration=None, season=None, season_uuid=None, number=None, rating=None, aired=None, expiry=None, stream=None):
|
||||
"""
|
||||
:type uuid: str
|
||||
:type nodeid: str
|
||||
@ -105,6 +108,7 @@ class Episode:
|
||||
:type title: str
|
||||
:type description: str
|
||||
:type cover: str
|
||||
:type background: str
|
||||
:type duration: int
|
||||
:type season: int
|
||||
:type season_uuid: str
|
||||
@ -112,6 +116,7 @@ class Episode:
|
||||
:type rating: str
|
||||
:type aired: datetime
|
||||
:type expiry: datetime
|
||||
:type stream: string
|
||||
"""
|
||||
self.uuid = uuid
|
||||
self.nodeid = nodeid
|
||||
@ -121,6 +126,7 @@ class Episode:
|
||||
self.title = title
|
||||
self.description = description
|
||||
self.cover = cover
|
||||
self.background = background
|
||||
self.duration = duration
|
||||
self.season = season
|
||||
self.season_uuid = season_uuid
|
||||
@ -128,6 +134,28 @@ class Episode:
|
||||
self.rating = rating
|
||||
self.aired = aired
|
||||
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):
|
||||
return "%r" % self.__dict__
|
||||
@ -196,7 +224,7 @@ class ContentApi:
|
||||
title=title))
|
||||
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.
|
||||
:type channel: str
|
||||
:type path: str
|
||||
@ -206,11 +234,18 @@ class ContentApi:
|
||||
if channel not in CHANNELS:
|
||||
raise Exception('Unknown channel %s' % channel)
|
||||
|
||||
# We want to use the html to extract clips
|
||||
# This is the worst hack, since Python 2.7 doesn't support nonlocal
|
||||
raw_html = [None]
|
||||
|
||||
def update():
|
||||
""" Fetch the program metadata by scraping """
|
||||
# Fetch webpage
|
||||
page = self._get_url(CHANNELS[channel]['url'] + '/' + path)
|
||||
|
||||
# Store a copy in the parent's raw_html var.
|
||||
raw_html[0] = page
|
||||
|
||||
# Extract JSON
|
||||
regex_program = re.compile(r'data-hero="([^"]+)', re.DOTALL)
|
||||
json_data = HTMLParser().unescape(regex_program.search(page).group(1))
|
||||
@ -220,40 +255,79 @@ class ContentApi:
|
||||
|
||||
# Fetch listing from cache or update if needed
|
||||
data = self._handle_cache(key=['program', channel, path], cache_mode=cache, update=update)
|
||||
if not data:
|
||||
return None
|
||||
|
||||
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
|
||||
|
||||
def get_episode(self, channel, path):
|
||||
def get_episode(self, channel, path, cache=CACHE_AUTO):
|
||||
""" Get a Episode object from the specified page.
|
||||
:type channel: str
|
||||
:type path: str
|
||||
:type cache: str
|
||||
:rtype Episode
|
||||
NOTE: This function doesn't use an API.
|
||||
"""
|
||||
if channel not in CHANNELS:
|
||||
raise Exception('Unknown channel %s' % channel)
|
||||
|
||||
def update():
|
||||
""" Fetch the program metadata by scraping """
|
||||
# Load webpage
|
||||
page = self._get_url(CHANNELS[channel]['url'] + '/' + path)
|
||||
|
||||
# Extract program JSON
|
||||
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)
|
||||
json_data = parser.unescape(regex_program.search(page).group(1))
|
||||
data = json.loads(json_data)['data']
|
||||
program = self._parse_program_data(data)
|
||||
result = regex_program.search(page)
|
||||
if result:
|
||||
program_json_data = parser.unescape(result.group(1))
|
||||
program_json = json.loads(program_json_data)['data']
|
||||
|
||||
# Extract episode JSON
|
||||
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))
|
||||
data = json.loads(json_data)
|
||||
result = regex_episode.search(page)
|
||||
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
|
||||
# The episode we just found doesn't contain all information
|
||||
return dict(program=program_json, episode=episode_json)
|
||||
|
||||
# Fetch listing from cache or update if needed
|
||||
data = self._handle_cache(key=['episode', channel, path], cache_mode=cache, update=update)
|
||||
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:
|
||||
if episode.nodeid == data['pageInfo']['nodeId']:
|
||||
if episode.nodeid == data['episode']['pageInfo']['nodeId']:
|
||||
return episode
|
||||
|
||||
return None
|
||||
@ -267,15 +341,146 @@ class ContentApi:
|
||||
data = json.loads(response)
|
||||
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
|
||||
def _parse_program_data(data):
|
||||
""" Parse the Program JSON.
|
||||
:type data: dict
|
||||
:rtype Program
|
||||
"""
|
||||
if data is None:
|
||||
return None
|
||||
|
||||
# Create Program info
|
||||
program = Program(
|
||||
uuid=data['id'],
|
||||
@ -311,7 +516,7 @@ class ContentApi:
|
||||
return program
|
||||
|
||||
@staticmethod
|
||||
def _parse_episode_data(data, season_uuid):
|
||||
def _parse_episode_data(data, season_uuid=None):
|
||||
""" Parse the Episode JSON.
|
||||
:type data: dict
|
||||
:type season_uuid: str
|
||||
@ -337,13 +542,15 @@ class ContentApi:
|
||||
title=data.get('title'),
|
||||
description=data.get('pageInfo', {}).get('description'),
|
||||
cover=data.get('image'),
|
||||
background=data.get('image'),
|
||||
duration=data.get('duration'),
|
||||
season=data.get('seasonNumber'),
|
||||
season_uuid=season_uuid,
|
||||
number=episode_number,
|
||||
aired=datetime.fromtimestamp(data.get('createdDate')),
|
||||
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
|
||||
|
||||
@ -393,7 +600,7 @@ class ContentApi:
|
||||
|
||||
def _get_cache(self, key, allow_expired=False):
|
||||
""" Get an item from the cache """
|
||||
filename = '.'.join(key) + '.json'
|
||||
filename = ('.'.join(key) + '.json').replace('/', '_')
|
||||
fullpath = self._cache_path + filename
|
||||
|
||||
if not os.path.exists(fullpath):
|
||||
@ -412,7 +619,7 @@ class ContentApi:
|
||||
|
||||
def _set_cache(self, key, data, ttl):
|
||||
""" Store an item in the cache """
|
||||
filename = '.'.join(key) + '.json'
|
||||
filename = ('.'.join(key) + '.json').replace('/', '_')
|
||||
fullpath = self._cache_path + filename
|
||||
|
||||
if not os.path.exists(self._cache_path):
|
||||
|
@ -10,7 +10,7 @@ import unittest
|
||||
|
||||
import resources.lib.kodiutils as kodiutils
|
||||
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')
|
||||
|
||||
@ -27,14 +27,31 @@ class TestApi(unittest.TestCase):
|
||||
self.assertIsInstance(programs, list)
|
||||
self.assertIsInstance(programs[0], Program)
|
||||
|
||||
def test_categories(self):
|
||||
for channel in ['vier', 'vijf', 'zes']:
|
||||
categories = self._api.get_categories(channel)
|
||||
self.assertIsInstance(categories, list)
|
||||
if categories:
|
||||
self.assertIsInstance(categories[0], Category)
|
||||
|
||||
def test_episodes(self):
|
||||
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.seasons, dict)
|
||||
self.assertIsInstance(program.episodes, list)
|
||||
self.assertIsInstance(program.episodes[0], Episode)
|
||||
|
||||
def test_clips(self):
|
||||
for channel, program in [('vier', 'gert-late-night'), ('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.')
|
||||
def test_get_stream(self):
|
||||
program = self._api.get_program('vier', 'auwch')
|
||||
|
@ -40,7 +40,7 @@ class TestRouting(unittest.TestCase):
|
||||
routing.run([routing.url_for(addon.show_catalog), '0', ''])
|
||||
|
||||
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):
|
||||
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', ''])
|
||||
|
||||
def test_tvguide_menu(self):
|
||||
routing.run([routing.url_for(addon.show_tvguide_channel, 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, channel='vier'), '0', ''])
|
||||
routing.run([routing.url_for(addon.show_channel_tvguide_detail, channel='vier', date='today'), '0', ''])
|
||||
|
||||
def test_metadata_update(self):
|
||||
routing.run([routing.url_for(addon.metadata_update), '0', ''])
|
||||
|
Loading…
Reference in New Issue
Block a user