Implement program caching for more metadata in listings (#8)
This commit is contained in:
parent
0ca9cf5b75
commit
7e86106562
2
Makefile
2
Makefile
@ -7,7 +7,7 @@ version = $(shell xmllint --xpath 'string(/addon/@version)' addon.xml)
|
|||||||
git_branch = $(shell git rev-parse --abbrev-ref HEAD)
|
git_branch = $(shell git rev-parse --abbrev-ref HEAD)
|
||||||
git_hash = $(shell git rev-parse --short HEAD)
|
git_hash = $(shell git rev-parse --short HEAD)
|
||||||
zip_name = $(name)-$(version)-$(git_branch)-$(git_hash).zip
|
zip_name = $(name)-$(version)-$(git_branch)-$(git_hash).zip
|
||||||
include_files = addon_entry.py addon.xml CHANGELOG.md LICENSE README.md resources/
|
include_files = addon_entry.py addon.xml CHANGELOG.md LICENSE README.md service_entry.py resources/
|
||||||
include_paths = $(patsubst %,$(name)/%,$(include_files))
|
include_paths = $(patsubst %,$(name)/%,$(include_files))
|
||||||
exclude_files = \*.new \*.orig \*.pyc \*.pyo
|
exclude_files = \*.new \*.orig \*.pyc \*.pyo
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
<extension point="xbmc.python.pluginsource" library="addon_entry.py">
|
<extension point="xbmc.python.pluginsource" library="addon_entry.py">
|
||||||
<provides>video</provides>
|
<provides>video</provides>
|
||||||
</extension>
|
</extension>
|
||||||
|
<extension point="xbmc.service" library="service_entry.py"/>
|
||||||
<extension point="xbmc.addon.metadata">
|
<extension point="xbmc.addon.metadata">
|
||||||
<summary lang="en_GB">Watch content from VIER, VIJF and ZES.</summary>
|
<summary lang="en_GB">Watch content from VIER, VIJF and ZES.</summary>
|
||||||
<platform>all</platform>
|
<platform>all</platform>
|
||||||
|
@ -120,6 +120,18 @@ msgctxt "#30713"
|
|||||||
msgid "The requested video was not found in the guide."
|
msgid "The requested video was not found in the guide."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30714"
|
||||||
|
msgid "Local metadata is cleared."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30715"
|
||||||
|
msgid "Updating metadata"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30716"
|
||||||
|
msgid "Updating metadata ({index}/{total})..."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgctxt "#30717"
|
msgctxt "#30717"
|
||||||
msgid "This program is not available in the catalogue."
|
msgid "This program is not available in the catalogue."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -141,3 +153,24 @@ msgstr ""
|
|||||||
msgctxt "#30805"
|
msgctxt "#30805"
|
||||||
msgid "Password"
|
msgid "Password"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30820"
|
||||||
|
msgid "Interface"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30827"
|
||||||
|
msgid "Metadata"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30829"
|
||||||
|
msgid "Periodically refresh metadata in the background"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30831"
|
||||||
|
msgid "Update local metadata now"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30833"
|
||||||
|
msgid "Clear local metadata"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
@ -121,6 +121,18 @@ msgctxt "#30713"
|
|||||||
msgid "The requested video was not found in the guide."
|
msgid "The requested video was not found in the guide."
|
||||||
msgstr "De gevraagde video werd niet gevonden in de tv-gids."
|
msgstr "De gevraagde video werd niet gevonden in de tv-gids."
|
||||||
|
|
||||||
|
msgctxt "#30714"
|
||||||
|
msgid "Local metadata is cleared."
|
||||||
|
msgstr "De locale metadata is verwijderd."
|
||||||
|
|
||||||
|
msgctxt "#30715"
|
||||||
|
msgid "Updating metadata"
|
||||||
|
msgstr "Vernieuwen metadata"
|
||||||
|
|
||||||
|
msgctxt "#30716"
|
||||||
|
msgid "Updating metadata ({index}/{total})..."
|
||||||
|
msgstr "Vernieuwen metadata ({index}/{total})..."
|
||||||
|
|
||||||
msgctxt "#30717"
|
msgctxt "#30717"
|
||||||
msgid "This program is not available in the catalogue."
|
msgid "This program is not available in the catalogue."
|
||||||
msgstr "Dit programma is niet beschikbaar in de catalogus."
|
msgstr "Dit programma is niet beschikbaar in de catalogus."
|
||||||
@ -142,3 +154,24 @@ msgstr "E-mailadres"
|
|||||||
msgctxt "#30805"
|
msgctxt "#30805"
|
||||||
msgid "Password"
|
msgid "Password"
|
||||||
msgstr "Wachtwoord"
|
msgstr "Wachtwoord"
|
||||||
|
|
||||||
|
msgctxt "#30820"
|
||||||
|
msgid "Interface"
|
||||||
|
msgstr "Interface"
|
||||||
|
|
||||||
|
msgctxt "#30827"
|
||||||
|
msgid "Metadata"
|
||||||
|
msgstr "Metadata"
|
||||||
|
|
||||||
|
msgctxt "#30829"
|
||||||
|
msgid "Periodically refresh metadata in the background"
|
||||||
|
msgstr "Vernieuw de metdata automatisch in de achtergrond"
|
||||||
|
|
||||||
|
msgctxt "#30831"
|
||||||
|
msgid "Update local metadata now"
|
||||||
|
msgstr "De locale metadata nu vernieuwen"
|
||||||
|
|
||||||
|
msgctxt "#30833"
|
||||||
|
msgid "Clear local metadata"
|
||||||
|
msgstr "De locale metadata verwijderen"
|
||||||
|
|
||||||
|
@ -3,12 +3,15 @@
|
|||||||
|
|
||||||
from __future__ import absolute_import, division, unicode_literals
|
from __future__ import absolute_import, division, unicode_literals
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
from routing import Plugin
|
from routing import Plugin
|
||||||
|
|
||||||
from resources.lib import kodilogging
|
from resources.lib import kodilogging
|
||||||
|
|
||||||
kodilogging.config()
|
kodilogging.config()
|
||||||
routing = Plugin()
|
routing = Plugin()
|
||||||
|
_LOGGER = logging.getLogger('addon')
|
||||||
|
|
||||||
|
|
||||||
@routing.route('/')
|
@routing.route('/')
|
||||||
@ -67,11 +70,11 @@ def show_catalog_program(channel, program):
|
|||||||
Catalog().show_program(channel, program)
|
Catalog().show_program(channel, program)
|
||||||
|
|
||||||
|
|
||||||
@routing.route('/program/program/<channel>/<program>/<season>')
|
@routing.route('/catalog/program/<channel>/<program>/<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 program from the catalog """
|
||||||
from resources.lib.modules.catalog import Catalog
|
from resources.lib.modules.catalog import Catalog
|
||||||
Catalog().show_program_season(channel, program, int(season))
|
Catalog().show_program_season(channel, program, season)
|
||||||
|
|
||||||
|
|
||||||
@routing.route('/search')
|
@routing.route('/search')
|
||||||
@ -82,11 +85,11 @@ def show_search(query=None):
|
|||||||
Search().show_search(query)
|
Search().show_search(query)
|
||||||
|
|
||||||
|
|
||||||
@routing.route('/play/catalog/<channel>/<uuid>')
|
@routing.route('/play/catalog/<uuid>')
|
||||||
def play(channel, uuid):
|
def play(uuid):
|
||||||
""" Play the requested item """
|
""" Play the requested item """
|
||||||
from resources.lib.modules.player import Player
|
from resources.lib.modules.player import Player
|
||||||
Player().play(channel, uuid)
|
Player().play(uuid)
|
||||||
|
|
||||||
|
|
||||||
@routing.route('/play/page/<channel>/<page>')
|
@routing.route('/play/page/<channel>/<page>')
|
||||||
@ -101,6 +104,20 @@ def play_from_page(channel, page):
|
|||||||
Player().play_from_page(channel, unquote(page))
|
Player().play_from_page(channel, unquote(page))
|
||||||
|
|
||||||
|
|
||||||
|
@routing.route('/metadata/update')
|
||||||
|
def metadata_update():
|
||||||
|
""" Update the metadata for the listings (called from settings) """
|
||||||
|
from resources.lib.modules.metadata import Metadata
|
||||||
|
Metadata().update()
|
||||||
|
|
||||||
|
|
||||||
|
@routing.route('/metadata/clean')
|
||||||
|
def metadata_clean():
|
||||||
|
""" Clear metadata (called from settings) """
|
||||||
|
from resources.lib.modules.metadata import Metadata
|
||||||
|
Metadata().clean()
|
||||||
|
|
||||||
|
|
||||||
def run(params):
|
def run(params):
|
||||||
""" Run the routing plugin """
|
""" Run the routing plugin """
|
||||||
routing.run(params)
|
routing.run(params)
|
||||||
|
@ -16,13 +16,14 @@ ADDON = xbmcaddon.Addon()
|
|||||||
SORT_METHODS = dict(
|
SORT_METHODS = dict(
|
||||||
unsorted=xbmcplugin.SORT_METHOD_UNSORTED,
|
unsorted=xbmcplugin.SORT_METHOD_UNSORTED,
|
||||||
label=xbmcplugin.SORT_METHOD_LABEL_IGNORE_FOLDERS,
|
label=xbmcplugin.SORT_METHOD_LABEL_IGNORE_FOLDERS,
|
||||||
|
title=xbmcplugin.SORT_METHOD_TITLE,
|
||||||
episode=xbmcplugin.SORT_METHOD_EPISODE,
|
episode=xbmcplugin.SORT_METHOD_EPISODE,
|
||||||
duration=xbmcplugin.SORT_METHOD_DURATION,
|
duration=xbmcplugin.SORT_METHOD_DURATION,
|
||||||
year=xbmcplugin.SORT_METHOD_VIDEO_YEAR,
|
year=xbmcplugin.SORT_METHOD_VIDEO_YEAR,
|
||||||
date=xbmcplugin.SORT_METHOD_DATE,
|
date=xbmcplugin.SORT_METHOD_DATE,
|
||||||
)
|
)
|
||||||
DEFAULT_SORT_METHODS = [
|
DEFAULT_SORT_METHODS = [
|
||||||
'unsorted', 'label'
|
'unsorted', 'title'
|
||||||
]
|
]
|
||||||
|
|
||||||
_LOGGER = logging.getLogger('kodiutils')
|
_LOGGER = logging.getLogger('kodiutils')
|
||||||
@ -269,7 +270,7 @@ def set_locale():
|
|||||||
setlocale(LC_ALL, locale_lang)
|
setlocale(LC_ALL, locale_lang)
|
||||||
except (Error, ValueError) as exc:
|
except (Error, ValueError) as exc:
|
||||||
if locale_lang != 'en_GB':
|
if locale_lang != 'en_GB':
|
||||||
_LOGGER.debug("Your system does not support locale '{locale}': {error}", locale=locale_lang, error=exc)
|
_LOGGER.debug("Your system does not support locale '%s': %s", locale_lang, exc)
|
||||||
set_locale.cached = False
|
set_locale.cached = False
|
||||||
return False
|
return False
|
||||||
set_locale.cached = True
|
set_locale.cached = True
|
||||||
@ -423,14 +424,14 @@ def listdir(path):
|
|||||||
def mkdir(path):
|
def mkdir(path):
|
||||||
"""Create a directory (using xbmcvfs)"""
|
"""Create a directory (using xbmcvfs)"""
|
||||||
from xbmcvfs import mkdir as vfsmkdir
|
from xbmcvfs import mkdir as vfsmkdir
|
||||||
_LOGGER.debug("Create directory '{path}'.", path=path)
|
_LOGGER.debug("Create directory '%s'.", path)
|
||||||
return vfsmkdir(path)
|
return vfsmkdir(path)
|
||||||
|
|
||||||
|
|
||||||
def mkdirs(path):
|
def mkdirs(path):
|
||||||
"""Create directory including parents (using xbmcvfs)"""
|
"""Create directory including parents (using xbmcvfs)"""
|
||||||
from xbmcvfs import mkdirs as vfsmkdirs
|
from xbmcvfs import mkdirs as vfsmkdirs
|
||||||
_LOGGER.debug("Recursively create directory '{path}'.", path=path)
|
_LOGGER.debug("Recursively create directory '%s'.", path)
|
||||||
return vfsmkdirs(path)
|
return vfsmkdirs(path)
|
||||||
|
|
||||||
|
|
||||||
@ -458,14 +459,14 @@ def stat_file(path):
|
|||||||
def delete(path):
|
def delete(path):
|
||||||
"""Remove a file (using xbmcvfs)"""
|
"""Remove a file (using xbmcvfs)"""
|
||||||
from xbmcvfs import delete as vfsdelete
|
from xbmcvfs import delete as vfsdelete
|
||||||
_LOGGER.debug("Delete file '{path}'.", path=path)
|
_LOGGER.debug("Delete file '%s'.", path)
|
||||||
return vfsdelete(path)
|
return vfsdelete(path)
|
||||||
|
|
||||||
|
|
||||||
def container_refresh(url=None):
|
def container_refresh(url=None):
|
||||||
"""Refresh the current container or (re)load a container by URL"""
|
"""Refresh the current container or (re)load a container by URL"""
|
||||||
if url:
|
if url:
|
||||||
_LOGGER.debug('Execute: Container.Refresh({url})', url=url)
|
_LOGGER.debug('Execute: Container.Refresh(%s)', url)
|
||||||
xbmc.executebuiltin('Container.Refresh({url})'.format(url=url))
|
xbmc.executebuiltin('Container.Refresh({url})'.format(url=url))
|
||||||
else:
|
else:
|
||||||
_LOGGER.debug('Execute: Container.Refresh')
|
_LOGGER.debug('Execute: Container.Refresh')
|
||||||
@ -475,7 +476,7 @@ def container_refresh(url=None):
|
|||||||
def container_update(url):
|
def container_update(url):
|
||||||
"""Update the current container while respecting the path history."""
|
"""Update the current container while respecting the path history."""
|
||||||
if url:
|
if url:
|
||||||
_LOGGER.debug('Execute: Container.Update({url})', url=url)
|
_LOGGER.debug('Execute: Container.Update(%s)', url)
|
||||||
xbmc.executebuiltin('Container.Update({url})'.format(url=url))
|
xbmc.executebuiltin('Container.Update({url})'.format(url=url))
|
||||||
else:
|
else:
|
||||||
# URL is a mandatory argument for Container.Update, use Container.Refresh instead
|
# URL is a mandatory argument for Container.Update, use Container.Refresh instead
|
||||||
@ -529,7 +530,7 @@ def get_cache(key, ttl=None):
|
|||||||
|
|
||||||
with open_file(fullpath, 'r') as fdesc:
|
with open_file(fullpath, 'r') as fdesc:
|
||||||
try:
|
try:
|
||||||
_LOGGER.info('Fetching {file} from cache', file=filename)
|
_LOGGER.debug('Fetching %s from cache', filename)
|
||||||
import json
|
import json
|
||||||
value = json.load(fdesc)
|
value = json.load(fdesc)
|
||||||
return value
|
return value
|
||||||
@ -547,6 +548,21 @@ def set_cache(key, data):
|
|||||||
mkdirs(path)
|
mkdirs(path)
|
||||||
|
|
||||||
with open_file(fullpath, 'w') as fdesc:
|
with open_file(fullpath, 'w') as fdesc:
|
||||||
_LOGGER.info('Storing to cache as {file}', file=filename)
|
_LOGGER.debug('Storing to cache as %s', filename)
|
||||||
import json
|
import json
|
||||||
json.dump(data, fdesc)
|
json.dump(data, fdesc)
|
||||||
|
|
||||||
|
|
||||||
|
def invalidate_cache(ttl=None):
|
||||||
|
""" Clear the cache """
|
||||||
|
path = get_cache_path()
|
||||||
|
if not exists(path):
|
||||||
|
return
|
||||||
|
_, files = listdir(path)
|
||||||
|
import time
|
||||||
|
now = time.mktime(time.localtime())
|
||||||
|
for filename in files:
|
||||||
|
fullpath = path + filename
|
||||||
|
if ttl and now - stat_file(fullpath).st_mtime() < ttl:
|
||||||
|
continue
|
||||||
|
delete(fullpath)
|
||||||
|
@ -9,6 +9,7 @@ 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.modules.menu import Menu
|
||||||
from resources.lib.viervijfzes import CHANNELS
|
from resources.lib.viervijfzes import CHANNELS
|
||||||
|
from resources.lib.viervijfzes.auth import AuthApi
|
||||||
from resources.lib.viervijfzes.content import ContentApi, UnavailableException
|
from resources.lib.viervijfzes.content import ContentApi, UnavailableException
|
||||||
|
|
||||||
_LOGGER = logging.getLogger('catalog')
|
_LOGGER = logging.getLogger('catalog')
|
||||||
@ -19,7 +20,8 @@ class Catalog:
|
|||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
""" Initialise object """
|
""" Initialise object """
|
||||||
self._api = ContentApi()
|
auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password'))
|
||||||
|
self._api = ContentApi(auth)
|
||||||
self._menu = Menu()
|
self._menu = Menu()
|
||||||
|
|
||||||
def show_catalog(self):
|
def show_catalog(self):
|
||||||
@ -34,9 +36,9 @@ class Catalog:
|
|||||||
|
|
||||||
listing = [self._menu.generate_titleitem(item) for item in items]
|
listing = [self._menu.generate_titleitem(item) for item in items]
|
||||||
|
|
||||||
# Sort items by label, but don't put folders at the top.
|
# 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.
|
||||||
kodiutils.show_listing(listing, 30003, content='tvshows', sort='label')
|
kodiutils.show_listing(listing, 30003, content='tvshows', sort='title')
|
||||||
|
|
||||||
def show_catalog_channel(self, channel):
|
def show_catalog_channel(self, channel):
|
||||||
""" Show the programs of a specific channel
|
""" Show the programs of a specific channel
|
||||||
@ -52,9 +54,9 @@ class Catalog:
|
|||||||
for item in items:
|
for item in items:
|
||||||
listing.append(self._menu.generate_titleitem(item))
|
listing.append(self._menu.generate_titleitem(item))
|
||||||
|
|
||||||
# Sort items by label, but don't put folders at the top.
|
# 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.
|
||||||
kodiutils.show_listing(listing, 30003, content='tvshows', sort='label')
|
kodiutils.show_listing(listing, 30003, content='tvshows', sort='title')
|
||||||
|
|
||||||
def show_program(self, channel, program_id):
|
def show_program(self, channel, program_id):
|
||||||
""" Show a program from the catalog
|
""" Show a program from the catalog
|
||||||
@ -75,7 +77,7 @@ class Catalog:
|
|||||||
|
|
||||||
# Go directly to the season when we have only one season
|
# Go directly to the season when we have only one season
|
||||||
if len(program.seasons) == 1:
|
if len(program.seasons) == 1:
|
||||||
self.show_program_season(channel, program_id, program.seasons.values()[0].number)
|
self.show_program_season(channel, program_id, program.seasons.values()[0].uuid)
|
||||||
return
|
return
|
||||||
|
|
||||||
studio = CHANNELS.get(program.channel, {}).get('studio_icon')
|
studio = CHANNELS.get(program.channel, {}).get('studio_icon')
|
||||||
@ -87,9 +89,8 @@ class Catalog:
|
|||||||
listing.append(
|
listing.append(
|
||||||
TitleItem(
|
TitleItem(
|
||||||
title='* %s' % kodiutils.localize(30204), # * All seasons
|
title='* %s' % kodiutils.localize(30204), # * All seasons
|
||||||
path=kodiutils.url_for('show_catalog_program_season', channel=channel, program=program_id, season=-1),
|
path=kodiutils.url_for('show_catalog_program_season', channel=channel, program=program_id, season='-1'),
|
||||||
art_dict={
|
art_dict={
|
||||||
'thumb': program.cover,
|
|
||||||
'fanart': program.background,
|
'fanart': program.background,
|
||||||
},
|
},
|
||||||
info_dict={
|
info_dict={
|
||||||
@ -107,9 +108,8 @@ class Catalog:
|
|||||||
listing.append(
|
listing.append(
|
||||||
TitleItem(
|
TitleItem(
|
||||||
title=s.title, # kodiutils.localize(30205, season=s.number), # Season {season}
|
title=s.title, # kodiutils.localize(30205, season=s.number), # Season {season}
|
||||||
path=kodiutils.url_for('show_catalog_program_season', channel=channel, program=program_id, season=s.number),
|
path=kodiutils.url_for('show_catalog_program_season', channel=channel, program=program_id, season=s.uuid),
|
||||||
art_dict={
|
art_dict={
|
||||||
'thumb': s.cover,
|
|
||||||
'fanart': program.background,
|
'fanart': program.background,
|
||||||
},
|
},
|
||||||
info_dict={
|
info_dict={
|
||||||
@ -123,13 +123,13 @@ class Catalog:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Sort by label. Some programs return seasons unordered.
|
# Sort by label. Some programs return seasons unordered.
|
||||||
kodiutils.show_listing(listing, 30003, content='tvshows', sort=['label'])
|
kodiutils.show_listing(listing, 30003, content='tvshows')
|
||||||
|
|
||||||
def show_program_season(self, channel, program_id, season):
|
def show_program_season(self, channel, program_id, season_uuid):
|
||||||
""" Show the episodes of a program from the catalog
|
""" Show the episodes of a program from the catalog
|
||||||
:type channel: str
|
:type channel: str
|
||||||
:type program_id: str
|
:type program_id: str
|
||||||
:type season: int
|
:type season_uuid: str
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
program = self._api.get_program(channel, program_id)
|
program = self._api.get_program(channel, program_id)
|
||||||
@ -138,12 +138,12 @@ class Catalog:
|
|||||||
kodiutils.end_of_directory()
|
kodiutils.end_of_directory()
|
||||||
return
|
return
|
||||||
|
|
||||||
if season == -1:
|
if season_uuid == "-1":
|
||||||
# Show all episodes
|
# Show all episodes
|
||||||
episodes = program.episodes
|
episodes = program.episodes
|
||||||
else:
|
else:
|
||||||
# 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 == season]
|
episodes = [e for e in program.episodes if e.season_uuid == season_uuid]
|
||||||
|
|
||||||
listing = [self._menu.generate_titleitem(episode) for episode in episodes]
|
listing = [self._menu.generate_titleitem(episode) for episode in episodes]
|
||||||
|
|
||||||
|
@ -86,7 +86,14 @@ class Menu:
|
|||||||
'season': len(item.seasons) if item.seasons else None,
|
'season': len(item.seasons) if item.seasons else None,
|
||||||
})
|
})
|
||||||
|
|
||||||
return TitleItem(title=item.title,
|
if isinstance(item.episodes, list) and not item.episodes:
|
||||||
|
# We know that we don't have episodes
|
||||||
|
title = '[COLOR gray]' + item.title + '[/COLOR]'
|
||||||
|
else:
|
||||||
|
# We have episodes, or we don't know it
|
||||||
|
title = item.title
|
||||||
|
|
||||||
|
return TitleItem(title=title,
|
||||||
path=kodiutils.url_for('show_catalog_program', channel=item.channel, program=item.path),
|
path=kodiutils.url_for('show_catalog_program', channel=item.channel, program=item.path),
|
||||||
art_dict=art_dict,
|
art_dict=art_dict,
|
||||||
info_dict=info_dict)
|
info_dict=info_dict)
|
||||||
@ -113,7 +120,7 @@ class Menu:
|
|||||||
})
|
})
|
||||||
|
|
||||||
return TitleItem(title=info_dict['title'],
|
return TitleItem(title=info_dict['title'],
|
||||||
path=kodiutils.url_for('play', channel=item.channel, uuid=item.uuid),
|
path=kodiutils.url_for('play', uuid=item.uuid),
|
||||||
art_dict=art_dict,
|
art_dict=art_dict,
|
||||||
info_dict=info_dict,
|
info_dict=info_dict,
|
||||||
stream_dict=stream_dict,
|
stream_dict=stream_dict,
|
||||||
|
60
resources/lib/modules/metadata.py
Normal file
60
resources/lib/modules/metadata.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
""" Metadata module """
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, unicode_literals
|
||||||
|
|
||||||
|
from resources.lib import kodiutils
|
||||||
|
from resources.lib.viervijfzes import CHANNELS
|
||||||
|
from resources.lib.viervijfzes.content import ContentApi, Program
|
||||||
|
|
||||||
|
|
||||||
|
class Metadata:
|
||||||
|
""" Code responsible for the management of the local cached metadata """
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
""" Initialise object """
|
||||||
|
self._api = ContentApi()
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
""" Update the metadata with a foreground progress indicator """
|
||||||
|
# Create progress indicator
|
||||||
|
progress = kodiutils.progress(message=kodiutils.localize(30715)) # Updating metadata
|
||||||
|
|
||||||
|
def update_status(i, total):
|
||||||
|
""" Update the progress indicator """
|
||||||
|
progress.update(int(((i + 1) / total) * 100), kodiutils.localize(30716, index=i + 1, total=total)) # Updating metadata ({index}/{total})
|
||||||
|
return progress.iscanceled()
|
||||||
|
|
||||||
|
self.fetch_metadata(callback=update_status)
|
||||||
|
|
||||||
|
# Close progress indicator
|
||||||
|
progress.close()
|
||||||
|
|
||||||
|
def fetch_metadata(self, callback=None):
|
||||||
|
""" Fetch the metadata for all the items in the catalog
|
||||||
|
:type callback: callable
|
||||||
|
"""
|
||||||
|
# Fetch all items from the catalog
|
||||||
|
items = []
|
||||||
|
for channel in list(CHANNELS):
|
||||||
|
items.extend(self._api.get_programs(channel))
|
||||||
|
count = len(items)
|
||||||
|
|
||||||
|
# Loop over all of them and download the metadata
|
||||||
|
for index, item in enumerate(items):
|
||||||
|
if isinstance(item, Program):
|
||||||
|
self._api.get_program(item.channel, item.path)
|
||||||
|
|
||||||
|
# Run callback after every item
|
||||||
|
if callback and callback(index, count):
|
||||||
|
# Stop when callback returns False
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def clean():
|
||||||
|
""" Clear metadata (called from settings) """
|
||||||
|
kodiutils.invalidate_cache()
|
||||||
|
kodiutils.set_setting('metadata_last_updated', '0')
|
||||||
|
kodiutils.ok_dialog(message=kodiutils.localize(30714)) # Local metadata is cleared
|
@ -28,12 +28,11 @@ class Player:
|
|||||||
episode = ContentApi().get_episode(channel, path)
|
episode = ContentApi().get_episode(channel, path)
|
||||||
|
|
||||||
# Play this now we have the uuid
|
# Play this now we have the uuid
|
||||||
self.play(channel, episode.uuid)
|
self.play(episode.uuid)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def play(channel, item):
|
def play(item):
|
||||||
""" Play the requested item.
|
""" Play the requested item.
|
||||||
:type channel: string
|
|
||||||
:type item: string
|
:type item: string
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
@ -48,16 +47,16 @@ class Player:
|
|||||||
|
|
||||||
# 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'))
|
||||||
token = auth.get_token()
|
|
||||||
except (InvalidLoginException, AuthenticationException) as ex:
|
|
||||||
_LOGGER.error(ex)
|
|
||||||
kodiutils.ok_dialog(message=kodiutils.localize(30702, error=ex.message))
|
|
||||||
kodiutils.end_of_directory()
|
|
||||||
return
|
|
||||||
|
|
||||||
# Get stream information
|
# Get stream information
|
||||||
resolved_stream = ContentApi(token).get_stream(channel, item)
|
resolved_stream = ContentApi(auth).get_stream_by_uuid(item)
|
||||||
|
|
||||||
|
except (InvalidLoginException, AuthenticationException) as ex:
|
||||||
|
_LOGGER.error(ex)
|
||||||
|
kodiutils.ok_dialog(message=kodiutils.localize(30702, error=str(ex)))
|
||||||
|
kodiutils.end_of_directory()
|
||||||
|
return
|
||||||
|
|
||||||
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...
|
||||||
|
@ -170,4 +170,4 @@ class TvGuide:
|
|||||||
return
|
return
|
||||||
|
|
||||||
kodiutils.container_update(
|
kodiutils.container_update(
|
||||||
kodiutils.url_for('play', channel=channel, uuid=broadcast.video_url))
|
kodiutils.url_for('play', uuid=broadcast.video_url))
|
||||||
|
83
resources/lib/service.py
Normal file
83
resources/lib/service.py
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
""" Background service code """
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, unicode_literals
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import logging
|
||||||
|
from time import time
|
||||||
|
|
||||||
|
from xbmc import Monitor
|
||||||
|
|
||||||
|
from resources.lib import kodilogging, kodiutils
|
||||||
|
from resources.lib.viervijfzes.auth import AuthApi
|
||||||
|
|
||||||
|
kodilogging.config()
|
||||||
|
_LOGGER = logging.getLogger('service')
|
||||||
|
|
||||||
|
|
||||||
|
class BackgroundService(Monitor):
|
||||||
|
""" Background service code """
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
Monitor.__init__(self)
|
||||||
|
self.update_interval = 24 * 3600 # Every 24 hours
|
||||||
|
self.cache_expiry = 30 * 24 * 3600 # One month
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
""" Background loop for maintenance tasks """
|
||||||
|
_LOGGER.info('Service started')
|
||||||
|
|
||||||
|
while not self.abortRequested():
|
||||||
|
# Update every `update_interval` after the last update
|
||||||
|
if kodiutils.get_setting_bool('metadata_update') and int(kodiutils.get_setting('metadata_last_updated', 0)) + self.update_interval < time():
|
||||||
|
self._update_metadata()
|
||||||
|
|
||||||
|
# Stop when abort requested
|
||||||
|
if self.waitForAbort(10):
|
||||||
|
break
|
||||||
|
|
||||||
|
_LOGGER.info('Service stopped')
|
||||||
|
|
||||||
|
def onSettingsChanged(self):
|
||||||
|
""" Callback when a setting has changed """
|
||||||
|
if self._has_credentials_changed():
|
||||||
|
_LOGGER.info('Clearing auth tokens due to changed credentials')
|
||||||
|
AuthApi.clear_tokens()
|
||||||
|
|
||||||
|
# Refresh container
|
||||||
|
kodiutils.container_refresh()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _has_credentials_changed():
|
||||||
|
""" Check if credentials have changed """
|
||||||
|
old_hash = kodiutils.get_setting('credentials_hash')
|
||||||
|
new_hash = ''
|
||||||
|
if kodiutils.get_setting('username') or kodiutils.get_setting('password'):
|
||||||
|
new_hash = hashlib.md5((kodiutils.get_setting('username') + kodiutils.get_setting('password')).encode('utf-8')).hexdigest()
|
||||||
|
if new_hash != old_hash:
|
||||||
|
kodiutils.set_setting('credentials_hash', new_hash)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _update_metadata(self):
|
||||||
|
""" Update the metadata for the listings """
|
||||||
|
from resources.lib.modules.metadata import Metadata
|
||||||
|
|
||||||
|
# Clear outdated metadata
|
||||||
|
kodiutils.invalidate_cache(self.cache_expiry)
|
||||||
|
|
||||||
|
def update_status(_i, _total):
|
||||||
|
""" Allow to cancel the background job """
|
||||||
|
return self.abortRequested() or not kodiutils.get_setting_bool('metadata_update')
|
||||||
|
|
||||||
|
success = Metadata().fetch_metadata(callback=update_status)
|
||||||
|
|
||||||
|
# Update metadata_last_updated
|
||||||
|
if success:
|
||||||
|
kodiutils.set_setting('metadata_last_updated', str(int(time())))
|
||||||
|
|
||||||
|
|
||||||
|
def run():
|
||||||
|
""" Run the BackgroundService """
|
||||||
|
BackgroundService().run()
|
@ -5,9 +5,9 @@ from __future__ import absolute_import, division, unicode_literals
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
from resources.lib import kodiutils
|
||||||
from resources.lib.viervijfzes.auth_awsidp import AwsIdp, InvalidLoginException, AuthenticationException
|
from resources.lib.viervijfzes.auth_awsidp import AwsIdp, InvalidLoginException, AuthenticationException
|
||||||
|
|
||||||
_LOGGER = logging.getLogger('auth-api')
|
_LOGGER = logging.getLogger('auth-api')
|
||||||
@ -21,19 +21,18 @@ class AuthApi:
|
|||||||
|
|
||||||
TOKEN_FILE = 'auth-tokens.json'
|
TOKEN_FILE = 'auth-tokens.json'
|
||||||
|
|
||||||
def __init__(self, username, password, cache_dir=None):
|
def __init__(self, username, password):
|
||||||
""" Initialise object """
|
""" Initialise object """
|
||||||
self._username = username
|
self._username = username
|
||||||
self._password = password
|
self._password = password
|
||||||
self._cache_dir = cache_dir
|
self._cache_dir = kodiutils.get_tokens_path()
|
||||||
self._id_token = None
|
self._id_token = None
|
||||||
self._expiry = 0
|
self._expiry = 0
|
||||||
self._refresh_token = None
|
self._refresh_token = None
|
||||||
|
|
||||||
if self._cache_dir:
|
|
||||||
# Load tokens from cache
|
# Load tokens from cache
|
||||||
try:
|
try:
|
||||||
with open(self._cache_dir + self.TOKEN_FILE, 'rb') as f:
|
with kodiutils.open_file(self._cache_dir + self.TOKEN_FILE, 'rb') as f:
|
||||||
data_json = json.loads(f.read())
|
data_json = json.loads(f.read())
|
||||||
self._id_token = data_json.get('id_token')
|
self._id_token = data_json.get('id_token')
|
||||||
self._refresh_token = data_json.get('refresh_token')
|
self._refresh_token = data_json.get('refresh_token')
|
||||||
@ -59,7 +58,7 @@ class AuthApi:
|
|||||||
self._expiry = now + 3600
|
self._expiry = now + 3600
|
||||||
_LOGGER.debug('Got an id token by refreshing: %s', self._id_token)
|
_LOGGER.debug('Got an id token by refreshing: %s', self._id_token)
|
||||||
except (InvalidLoginException, AuthenticationException) as e:
|
except (InvalidLoginException, AuthenticationException) as e:
|
||||||
_LOGGER.error('Error logging in: %s', e.message)
|
_LOGGER.error('Error logging in: %s', str(e))
|
||||||
self._id_token = None
|
self._id_token = None
|
||||||
self._refresh_token = None
|
self._refresh_token = None
|
||||||
self._expiry = 0
|
self._expiry = 0
|
||||||
@ -74,12 +73,10 @@ class AuthApi:
|
|||||||
self._expiry = now + 3600
|
self._expiry = now + 3600
|
||||||
_LOGGER.debug('Got an id token by logging in: %s', self._id_token)
|
_LOGGER.debug('Got an id token by logging in: %s', self._id_token)
|
||||||
|
|
||||||
if self._cache_dir:
|
|
||||||
if not os.path.isdir(self._cache_dir):
|
|
||||||
os.mkdir(self._cache_dir)
|
|
||||||
|
|
||||||
# Store new tokens in cache
|
# Store new tokens in cache
|
||||||
with open(self._cache_dir + self.TOKEN_FILE, 'wb') as f:
|
if not kodiutils.exists(self._cache_dir):
|
||||||
|
kodiutils.mkdirs(self._cache_dir)
|
||||||
|
with kodiutils.open_file(self._cache_dir + self.TOKEN_FILE, 'wb') as f:
|
||||||
data = json.dumps(dict(
|
data = json.dumps(dict(
|
||||||
id_token=self._id_token,
|
id_token=self._id_token,
|
||||||
refresh_token=self._refresh_token,
|
refresh_token=self._refresh_token,
|
||||||
@ -89,18 +86,10 @@ class AuthApi:
|
|||||||
|
|
||||||
return self._id_token
|
return self._id_token
|
||||||
|
|
||||||
def clear_cache(self):
|
@staticmethod
|
||||||
|
def clear_tokens():
|
||||||
""" Remove the cached tokens. """
|
""" Remove the cached tokens. """
|
||||||
if not self._cache_dir:
|
kodiutils.delete(kodiutils.get_tokens_path() + AuthApi.TOKEN_FILE)
|
||||||
return
|
|
||||||
|
|
||||||
# Remove cache
|
|
||||||
os.remove(self._cache_dir + self.TOKEN_FILE)
|
|
||||||
|
|
||||||
# Clear tokens in memory
|
|
||||||
self._id_token = None
|
|
||||||
self._refresh_token = None
|
|
||||||
self._expiry = 0
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _authenticate(username, password):
|
def _authenticate(username, password):
|
||||||
|
@ -22,12 +22,10 @@ _LOGGER = logging.getLogger('auth-awsidp')
|
|||||||
|
|
||||||
class InvalidLoginException(Exception):
|
class InvalidLoginException(Exception):
|
||||||
""" The login credentials are invalid """
|
""" The login credentials are invalid """
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class AuthenticationException(Exception):
|
class AuthenticationException(Exception):
|
||||||
""" Something went wrong while logging in """
|
""" Something went wrong while logging in """
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class AwsIdp:
|
class AwsIdp:
|
||||||
@ -90,7 +88,7 @@ class AwsIdp:
|
|||||||
"Content-Type": "application/x-amz-json-1.1"
|
"Content-Type": "application/x-amz-json-1.1"
|
||||||
}
|
}
|
||||||
auth_response = self._session.post(self.url, auth_data, headers=auth_headers)
|
auth_response = self._session.post(self.url, auth_data, headers=auth_headers)
|
||||||
auth_response_json = json.loads(auth_response.content)
|
auth_response_json = json.loads(auth_response.text)
|
||||||
challenge_parameters = auth_response_json.get("ChallengeParameters")
|
challenge_parameters = auth_response_json.get("ChallengeParameters")
|
||||||
_LOGGER.debug(challenge_parameters)
|
_LOGGER.debug(challenge_parameters)
|
||||||
|
|
||||||
@ -106,7 +104,7 @@ class AwsIdp:
|
|||||||
"Content-Type": "application/x-amz-json-1.1"
|
"Content-Type": "application/x-amz-json-1.1"
|
||||||
}
|
}
|
||||||
auth_response = self._session.post(self.url, challenge_data, headers=challenge_headers)
|
auth_response = self._session.post(self.url, challenge_data, headers=challenge_headers)
|
||||||
auth_response_json = json.loads(auth_response.content)
|
auth_response_json = json.loads(auth_response.text)
|
||||||
_LOGGER.debug("Got response: %s", auth_response_json)
|
_LOGGER.debug("Got response: %s", auth_response_json)
|
||||||
|
|
||||||
if "message" in auth_response_json:
|
if "message" in auth_response_json:
|
||||||
@ -138,7 +136,7 @@ class AwsIdp:
|
|||||||
}
|
}
|
||||||
refresh_request_data = json.dumps(refresh_request)
|
refresh_request_data = json.dumps(refresh_request)
|
||||||
refresh_response = self._session.post(self.url, refresh_request_data, headers=refresh_headers)
|
refresh_response = self._session.post(self.url, refresh_request_data, headers=refresh_headers)
|
||||||
refresh_json = json.loads(refresh_response.content)
|
refresh_json = json.loads(refresh_response.text)
|
||||||
|
|
||||||
if "message" in refresh_json:
|
if "message" in refresh_json:
|
||||||
raise AuthenticationException(refresh_json.get("message"))
|
raise AuthenticationException(refresh_json.get("message"))
|
||||||
|
@ -11,10 +11,15 @@ from datetime import datetime
|
|||||||
from six.moves.html_parser import HTMLParser
|
from six.moves.html_parser import HTMLParser
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
from resources.lib import kodiutils
|
||||||
from resources.lib.viervijfzes import CHANNELS
|
from resources.lib.viervijfzes import CHANNELS
|
||||||
|
|
||||||
_LOGGER = logging.getLogger('content-api')
|
_LOGGER = logging.getLogger('content-api')
|
||||||
|
|
||||||
|
CACHE_AUTO = 1 # Allow to use the cache, and query the API if no cache is available
|
||||||
|
CACHE_ONLY = 2 # Only use the cache, don't use the API
|
||||||
|
CACHE_PREVENT = 3 # Don't use the cache
|
||||||
|
|
||||||
|
|
||||||
class UnavailableException(Exception):
|
class UnavailableException(Exception):
|
||||||
""" Is thrown when an item is unavailable. """
|
""" Is thrown when an item is unavailable. """
|
||||||
@ -89,8 +94,7 @@ 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, duration=None,
|
||||||
season=None, number=None,
|
season=None, season_uuid=None, number=None, rating=None, aired=None, expiry=None):
|
||||||
rating=None, aired=None, expiry=None):
|
|
||||||
"""
|
"""
|
||||||
:type uuid: str
|
:type uuid: str
|
||||||
:type nodeid: str
|
:type nodeid: str
|
||||||
@ -102,6 +106,7 @@ class Episode:
|
|||||||
:type cover: str
|
:type cover: str
|
||||||
:type duration: int
|
:type duration: int
|
||||||
:type season: int
|
:type season: int
|
||||||
|
:type season_uuid: str
|
||||||
:type number: int
|
:type number: int
|
||||||
:type rating: str
|
:type rating: str
|
||||||
:type aired: datetime
|
:type aired: datetime
|
||||||
@ -117,6 +122,7 @@ class Episode:
|
|||||||
self.cover = cover
|
self.cover = cover
|
||||||
self.duration = duration
|
self.duration = duration
|
||||||
self.season = season
|
self.season = season
|
||||||
|
self.season_uuid = season_uuid
|
||||||
self.number = number
|
self.number = number
|
||||||
self.rating = rating
|
self.rating = rating
|
||||||
self.aired = aired
|
self.aired = aired
|
||||||
@ -129,30 +135,69 @@ class Episode:
|
|||||||
class ContentApi:
|
class ContentApi:
|
||||||
""" VIER/VIJF/ZES Content API"""
|
""" VIER/VIJF/ZES Content API"""
|
||||||
API_ENDPOINT = 'https://api.viervijfzes.be'
|
API_ENDPOINT = 'https://api.viervijfzes.be'
|
||||||
|
SITE_APIS = {
|
||||||
|
'vier': 'https://www.vier.be/api',
|
||||||
|
'vijf': 'https://www.vijf.be/api',
|
||||||
|
'zes': 'https://www.zestv.be/api',
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, token=None):
|
def __init__(self, auth=None):
|
||||||
""" Initialise object """
|
""" Initialise object """
|
||||||
self._session = requests.session()
|
self._session = requests.session()
|
||||||
self._token = token
|
self._auth = auth
|
||||||
|
|
||||||
def get_notifications(self):
|
def get_notifications(self):
|
||||||
""" Get a list of notifications for your account.
|
""" Get a list of notifications for your account.
|
||||||
:rtype list[dict]
|
:rtype list[dict]
|
||||||
"""
|
"""
|
||||||
response = self._get_url(self.API_ENDPOINT + '/notifications')
|
response = self._get_url(self.API_ENDPOINT + '/notifications', authentication=True)
|
||||||
data = json.loads(response)
|
data = json.loads(response)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def get_stream(self, _channel, uuid):
|
def get_content_tree(self, channel):
|
||||||
|
""" Get a list of all the content.
|
||||||
|
:type channel: str
|
||||||
|
:rtype list[dict]
|
||||||
|
"""
|
||||||
|
if channel not in self.SITE_APIS:
|
||||||
|
raise Exception('Unknown channel %s' % channel)
|
||||||
|
|
||||||
|
response = self._get_url(self.SITE_APIS[channel] + '/content_tree', authentication=True)
|
||||||
|
data = json.loads(response)
|
||||||
|
return data
|
||||||
|
|
||||||
|
def get_stream_by_uuid(self, uuid):
|
||||||
""" Get the stream URL to use for this video.
|
""" Get the stream URL to use for this video.
|
||||||
:type _channel: str
|
|
||||||
:type uuid: str
|
:type uuid: str
|
||||||
:rtype str
|
:rtype str
|
||||||
"""
|
"""
|
||||||
response = self._get_url(self.API_ENDPOINT + '/content/%s' % uuid)
|
response = self._get_url(self.API_ENDPOINT + '/content/%s' % uuid, authentication=True)
|
||||||
data = json.loads(response)
|
data = json.loads(response)
|
||||||
return data['video']['S']
|
return data['video']['S']
|
||||||
|
|
||||||
|
def get_programs_new(self, channel):
|
||||||
|
""" Get a list of all programs of the specified channel.
|
||||||
|
:type channel: str
|
||||||
|
:rtype list[Program]
|
||||||
|
"""
|
||||||
|
if channel not in CHANNELS:
|
||||||
|
raise Exception('Unknown channel %s' % channel)
|
||||||
|
|
||||||
|
# Request all content from this channel
|
||||||
|
content_tree = self.get_content_tree(channel)
|
||||||
|
|
||||||
|
programs = []
|
||||||
|
for p in content_tree['programs']:
|
||||||
|
try:
|
||||||
|
program = self.get_program_by_uuid(p)
|
||||||
|
program.channel = channel
|
||||||
|
programs.append(program)
|
||||||
|
except UnavailableException:
|
||||||
|
# Some programs are not available, but do occur in the content tree
|
||||||
|
pass
|
||||||
|
|
||||||
|
return programs
|
||||||
|
|
||||||
def get_programs(self, channel):
|
def get_programs(self, channel):
|
||||||
""" Get a list of all programs of the specified channel.
|
""" Get a list of all programs of the specified channel.
|
||||||
:type channel: str
|
:type channel: str
|
||||||
@ -171,36 +216,90 @@ class ContentApi:
|
|||||||
r'<span class="program-overview__title">\s+(?P<title>[^<]+)</span>.*?'
|
r'<span class="program-overview__title">\s+(?P<title>[^<]+)</span>.*?'
|
||||||
r'</a>', re.DOTALL)
|
r'</a>', re.DOTALL)
|
||||||
|
|
||||||
programs = [
|
programs = []
|
||||||
Program(channel=channel,
|
for item in regex_programs.finditer(data):
|
||||||
path=program.group('path').lstrip('/'),
|
path = item.group('path').lstrip('/')
|
||||||
title=h.unescape(program.group('title').strip()))
|
|
||||||
for program in regex_programs.finditer(data)
|
program = self.get_program(channel, path, CACHE_ONLY) # Get program details, but from cache only
|
||||||
]
|
if program:
|
||||||
|
# Use program with metadata from cache
|
||||||
|
programs.append(program)
|
||||||
|
else:
|
||||||
|
# Use program with the values that we've parsed from the page
|
||||||
|
programs.append(Program(channel=channel,
|
||||||
|
path=path,
|
||||||
|
title=h.unescape(item.group('title').strip())))
|
||||||
|
|
||||||
return programs
|
return programs
|
||||||
|
|
||||||
def get_program(self, channel, path):
|
def get_program(self, channel, path, 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
|
||||||
|
:type cache: int
|
||||||
:rtype Program
|
:rtype Program
|
||||||
NOTE: This function doesn't use an API.
|
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)
|
||||||
|
|
||||||
# Load webpage
|
if cache in [CACHE_AUTO, CACHE_ONLY]:
|
||||||
|
# Try to fetch from cache
|
||||||
|
data = kodiutils.get_cache(['program', channel, path])
|
||||||
|
if data is None and cache == CACHE_ONLY:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
data = None
|
||||||
|
|
||||||
|
if data is None:
|
||||||
|
# Fetch webpage
|
||||||
page = self._get_url(CHANNELS[channel]['url'] + '/' + path)
|
page = self._get_url(CHANNELS[channel]['url'] + '/' + path)
|
||||||
|
|
||||||
# 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))
|
||||||
data = json.loads(json_data)['data']
|
data = json.loads(json_data)['data']
|
||||||
|
|
||||||
|
# Store response in cache
|
||||||
|
kodiutils.set_cache(['program', channel, path], data)
|
||||||
|
|
||||||
program = self._parse_program_data(data)
|
program = self._parse_program_data(data)
|
||||||
|
|
||||||
return program
|
return program
|
||||||
|
|
||||||
|
def get_program_by_uuid(self, uuid, cache=CACHE_AUTO):
|
||||||
|
""" Get a Program object.
|
||||||
|
:type uuid: str
|
||||||
|
:type cache: int
|
||||||
|
:rtype Program
|
||||||
|
"""
|
||||||
|
if cache in [CACHE_AUTO, CACHE_ONLY]:
|
||||||
|
# Try to fetch from cache
|
||||||
|
data = kodiutils.get_cache(['program', uuid])
|
||||||
|
if data is None and cache == CACHE_ONLY:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
data = None
|
||||||
|
|
||||||
|
if data is None:
|
||||||
|
# Fetch from API
|
||||||
|
response = self._get_url(self.API_ENDPOINT + '/content/%s' % uuid, authentication=True)
|
||||||
|
data = json.loads(response)
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
raise UnavailableException()
|
||||||
|
|
||||||
|
# Store response in cache
|
||||||
|
kodiutils.set_cache(['program', uuid], data)
|
||||||
|
|
||||||
|
return Program(
|
||||||
|
uuid=uuid,
|
||||||
|
path=data['url']['S'].strip('/'),
|
||||||
|
title=data['label']['S'],
|
||||||
|
description=data['description']['S'],
|
||||||
|
cover=data['image']['S'],
|
||||||
|
)
|
||||||
|
|
||||||
def get_episode(self, channel, path):
|
def get_episode(self, channel, path):
|
||||||
""" Get a Episode object from the specified page.
|
""" Get a Episode object from the specified page.
|
||||||
:type channel: str
|
:type channel: str
|
||||||
@ -254,7 +353,7 @@ class ContentApi:
|
|||||||
|
|
||||||
# Create Season info
|
# Create Season info
|
||||||
program.seasons = {
|
program.seasons = {
|
||||||
playlist['episodes'][0]['seasonNumber']: Season(
|
key: Season(
|
||||||
uuid=playlist['id'],
|
uuid=playlist['id'],
|
||||||
path=playlist['link'].lstrip('/'),
|
path=playlist['link'].lstrip('/'),
|
||||||
channel=playlist['pageInfo']['site'],
|
channel=playlist['pageInfo']['site'],
|
||||||
@ -262,12 +361,12 @@ class ContentApi:
|
|||||||
description=playlist['pageInfo']['description'],
|
description=playlist['pageInfo']['description'],
|
||||||
number=playlist['episodes'][0]['seasonNumber'], # You did not see this
|
number=playlist['episodes'][0]['seasonNumber'], # You did not see this
|
||||||
)
|
)
|
||||||
for playlist in data['playlists']
|
for key, playlist in enumerate(data['playlists'])
|
||||||
}
|
}
|
||||||
|
|
||||||
# Create Episodes info
|
# Create Episodes info
|
||||||
program.episodes = [
|
program.episodes = [
|
||||||
ContentApi._parse_episode_data(episode)
|
ContentApi._parse_episode_data(episode, playlist['id'])
|
||||||
for playlist in data['playlists']
|
for playlist in data['playlists']
|
||||||
for episode in playlist['episodes']
|
for episode in playlist['episodes']
|
||||||
]
|
]
|
||||||
@ -275,9 +374,10 @@ class ContentApi:
|
|||||||
return program
|
return program
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _parse_episode_data(data):
|
def _parse_episode_data(data, season_uuid):
|
||||||
""" Parse the Episode JSON.
|
""" Parse the Episode JSON.
|
||||||
:type data: dict
|
:type data: dict
|
||||||
|
:type season_uuid: str
|
||||||
:rtype Episode
|
:rtype Episode
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -291,22 +391,18 @@ class ContentApi:
|
|||||||
else:
|
else:
|
||||||
episode_number = None
|
episode_number = None
|
||||||
|
|
||||||
if data.get('episodeTitle'):
|
|
||||||
episode_title = data.get('episodeTitle')
|
|
||||||
else:
|
|
||||||
episode_title = data.get('title')
|
|
||||||
|
|
||||||
episode = Episode(
|
episode = Episode(
|
||||||
uuid=data.get('videoUuid'),
|
uuid=data.get('videoUuid'),
|
||||||
nodeid=data.get('pageInfo', {}).get('nodeId'),
|
nodeid=data.get('pageInfo', {}).get('nodeId'),
|
||||||
path=data.get('link').lstrip('/'),
|
path=data.get('link').lstrip('/'),
|
||||||
channel=data.get('pageInfo', {}).get('site'),
|
channel=data.get('pageInfo', {}).get('site'),
|
||||||
program_title=data.get('program', {}).get('title'),
|
program_title=data.get('program', {}).get('title'),
|
||||||
title=episode_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'),
|
||||||
duration=data.get('duration'),
|
duration=data.get('duration'),
|
||||||
season=data.get('seasonNumber'),
|
season=data.get('seasonNumber'),
|
||||||
|
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,
|
||||||
@ -314,19 +410,22 @@ class ContentApi:
|
|||||||
)
|
)
|
||||||
return episode
|
return episode
|
||||||
|
|
||||||
def _get_url(self, url, params=None):
|
def _get_url(self, url, params=None, authentication=False):
|
||||||
""" Makes a GET request for the specified URL.
|
""" Makes a GET request for the specified URL.
|
||||||
:type url: str
|
:type url: str
|
||||||
:rtype str
|
:rtype str
|
||||||
"""
|
"""
|
||||||
if self._token:
|
if authentication:
|
||||||
|
if not self._auth:
|
||||||
|
raise Exception('Requested to authenticate, but not auth object passed')
|
||||||
response = self._session.get(url, params=params, headers={
|
response = self._session.get(url, params=params, headers={
|
||||||
'authorization': self._token,
|
'authorization': self._auth.get_token(),
|
||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
response = self._session.get(url, params=params)
|
response = self._session.get(url, params=params)
|
||||||
|
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
|
_LOGGER.error(response.text)
|
||||||
raise Exception('Could not fetch data')
|
raise Exception('Could not fetch data')
|
||||||
|
|
||||||
return response.text
|
return response.text
|
||||||
|
@ -42,7 +42,7 @@ class SearchApi:
|
|||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
raise Exception('Could not search')
|
raise Exception('Could not search')
|
||||||
|
|
||||||
data = json.loads(response.content)
|
data = json.loads(response.text)
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
for hit in data['hits']['hits']:
|
for hit in data['hits']['hits']:
|
||||||
|
@ -1,8 +1,15 @@
|
|||||||
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||||
<settings>
|
<settings>
|
||||||
|
<setting id="metadata_last_updated" visible="false"/>
|
||||||
<category label="30800"> <!-- Credentials -->
|
<category label="30800"> <!-- Credentials -->
|
||||||
<setting label="30801" type="lsep"/> <!-- Credentials -->
|
<setting label="30801" type="lsep"/> <!-- Credentials -->
|
||||||
<setting label="30803" type="text" id="username"/>
|
<setting label="30803" type="text" id="username"/>
|
||||||
<setting label="30805" type="text" id="password" option="hidden"/>
|
<setting label="30805" type="text" id="password" option="hidden"/>
|
||||||
</category>
|
</category>
|
||||||
|
<category label="30820"> <!-- Interface -->
|
||||||
|
<setting label="30827" type="lsep"/> <!-- Metadata -->
|
||||||
|
<setting label="30829" type="bool" id="metadata_update" default="true" subsetting="true"/>
|
||||||
|
<setting label="30831" type="action" action="RunPlugin(plugin://plugin.video.viervijfzes/metadata/update)"/>
|
||||||
|
<setting label="30833" type="action" action="RunPlugin(plugin://plugin.video.viervijfzes/metadata/clean)"/>
|
||||||
|
</category>
|
||||||
</settings>
|
</settings>
|
||||||
|
8
service_entry.py
Normal file
8
service_entry.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
""" Service entry point """
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, unicode_literals
|
||||||
|
|
||||||
|
from resources.lib import service
|
||||||
|
|
||||||
|
service.run()
|
@ -9,8 +9,8 @@ import logging
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import resources.lib.kodiutils as kodiutils
|
import resources.lib.kodiutils as kodiutils
|
||||||
from resources.lib.viervijfzes.content import ContentApi, Program, Episode
|
|
||||||
from resources.lib.viervijfzes.auth import AuthApi
|
from resources.lib.viervijfzes.auth import AuthApi
|
||||||
|
from resources.lib.viervijfzes.content import ContentApi, Program, Episode
|
||||||
|
|
||||||
_LOGGER = logging.getLogger('test-api')
|
_LOGGER = logging.getLogger('test-api')
|
||||||
|
|
||||||
@ -18,7 +18,8 @@ _LOGGER = logging.getLogger('test-api')
|
|||||||
class TestApi(unittest.TestCase):
|
class TestApi(unittest.TestCase):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(TestApi, self).__init__(*args, **kwargs)
|
super(TestApi, self).__init__(*args, **kwargs)
|
||||||
self._api = ContentApi()
|
auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password'))
|
||||||
|
self._api = ContentApi(auth)
|
||||||
|
|
||||||
def test_programs(self):
|
def test_programs(self):
|
||||||
for channel in ['vier', 'vijf', 'zes']:
|
for channel in ['vier', 'vijf', 'zes']:
|
||||||
@ -39,9 +40,7 @@ class TestApi(unittest.TestCase):
|
|||||||
program = self._api.get_program('vier', 'auwch')
|
program = self._api.get_program('vier', 'auwch')
|
||||||
episode = program.episodes[0]
|
episode = program.episodes[0]
|
||||||
|
|
||||||
auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password'), kodiutils.get_tokens_path())
|
video = self._api.get_stream_by_uuid(episode.uuid)
|
||||||
api_authed = ContentApi(auth.get_token())
|
|
||||||
video = api_authed.get_stream(episode.channel, episode.uuid)
|
|
||||||
self.assertTrue(video)
|
self.assertTrue(video)
|
||||||
|
|
||||||
_LOGGER.info('Got video URL: %s', video)
|
_LOGGER.info('Got video URL: %s', video)
|
||||||
|
@ -17,18 +17,19 @@ _LOGGER = logging.getLogger('test-auth')
|
|||||||
class TestAuth(unittest.TestCase):
|
class TestAuth(unittest.TestCase):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(TestAuth, self).__init__(*args, **kwargs)
|
super(TestAuth, self).__init__(*args, **kwargs)
|
||||||
self._auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password'), kodiutils.get_tokens_path())
|
|
||||||
|
|
||||||
def test_login(self):
|
def test_login(self):
|
||||||
# Clear any cache we have
|
# Clear any cache we have
|
||||||
self._auth.clear_cache()
|
AuthApi.clear_tokens()
|
||||||
|
|
||||||
# We should get a token by logging in
|
# We should get a token by logging in
|
||||||
token = self._auth.get_token()
|
auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password'))
|
||||||
|
token = auth.get_token()
|
||||||
self.assertTrue(token)
|
self.assertTrue(token)
|
||||||
|
|
||||||
# Test it a second time, it should go from memory now
|
# Test it a second time, it should go from memory now
|
||||||
token = self._auth.get_token()
|
auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password'))
|
||||||
|
token = auth.get_token()
|
||||||
self.assertTrue(token)
|
self.assertTrue(token)
|
||||||
|
|
||||||
|
|
||||||
|
@ -49,15 +49,15 @@ class TestEpg(unittest.TestCase):
|
|||||||
epg_programs = self._epg.get_epg('vier', date.today().strftime('%Y-%m-%d'))
|
epg_programs = self._epg.get_epg('vier', date.today().strftime('%Y-%m-%d'))
|
||||||
epg_program = [program for program in epg_programs if program.video_url][0]
|
epg_program = [program for program in epg_programs if program.video_url][0]
|
||||||
|
|
||||||
|
auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password'))
|
||||||
|
api = ContentApi(auth)
|
||||||
|
|
||||||
# Lookup the Episode data since we don't have an UUID
|
# Lookup the Episode data since we don't have an UUID
|
||||||
api = ContentApi()
|
|
||||||
episode = api.get_episode(epg_program.channel, epg_program.video_url)
|
episode = api.get_episode(epg_program.channel, epg_program.video_url)
|
||||||
self.assertIsInstance(episode, Episode)
|
self.assertIsInstance(episode, Episode)
|
||||||
|
|
||||||
# Get stream based on the Episode's UUID
|
# Get stream based on the Episode's UUID
|
||||||
auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password'), kodiutils.get_tokens_path())
|
video = api.get_stream_by_uuid(episode.uuid)
|
||||||
api = ContentApi(auth.get_token())
|
|
||||||
video = api.get_stream(episode.channel, episode.uuid)
|
|
||||||
self.assertTrue(video)
|
self.assertTrue(video)
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user