Skip to content

Commit

Permalink
Fix favorites (#930)
Browse files Browse the repository at this point in the history
  • Loading branch information
mediaminister authored Jan 21, 2022
1 parent d9ec71f commit 2b5c162
Show file tree
Hide file tree
Showing 7 changed files with 178 additions and 112 deletions.
14 changes: 8 additions & 6 deletions resources/lib/addon.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,19 +43,21 @@ def delete_tokens():
TokenResolver().delete_tokens()


@plugin.route('/follow/<program>/<title>')
def follow(program, title):
@plugin.route('/follow/<program_name>/<title>')
@plugin.route('/follow/<program_name>/<program_id>/<title>')
def follow(program_name, title, program_id=None):
"""The API interface to follow a program used by the context menu"""
from favorites import Favorites
Favorites().follow(program=program, title=to_unicode(unquote_plus(from_unicode(title))))
Favorites().follow(program_name=program_name, title=to_unicode(unquote_plus(from_unicode(title))), program_id=program_id)


@plugin.route('/unfollow/<program>/<title>')
def unfollow(program, title):
@plugin.route('/unfollow/<program_name>/<title>')
@plugin.route('/unfollow/<program_name>/<program_id>/<title>')
def unfollow(program_name, title, program_id=None):
"""The API interface to unfollow a program used by the context menu"""
move_down = bool(plugin.args.get('move_down'))
from favorites import Favorites
Favorites().unfollow(program=program, title=to_unicode(unquote_plus(from_unicode(title))), move_down=move_down)
Favorites().unfollow(program_name=program_name, title=to_unicode(unquote_plus(from_unicode(title))), program_id=program_id, move_down=move_down)


@plugin.route('/watchlater/<episode_id>/<title>')
Expand Down
200 changes: 134 additions & 66 deletions resources/lib/favorites.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,25 @@
from __future__ import absolute_import, division, unicode_literals

try: # Python 3
from urllib.error import HTTPError
from urllib.parse import unquote
except ImportError: # Python 2
from urllib2 import unquote

from kodiutils import (container_refresh, get_cache, get_setting_bool, get_url_json,
has_credentials, input_down, invalidate_caches, localize, log_error,
has_credentials, input_down, invalidate_caches, localize,
multiselect, notification, ok_dialog, update_cache)
from utils import program_to_id


class Favorites:
"""Track, cache and manage VRT favorites"""

GRAPHQL_URL = 'https://www.vrt.be/vrtnu-api/graphql/v1'
FAVORITES_REST_URL = 'https://www.vrt.be/vrtnu-api/rest/lists/vrtnu-favoritePrograms'
FAVORITES_CACHE_FILE = 'favorites.json'

def __init__(self):
"""Initialize favorites, relies on XBMC vfs and a special VRT token"""
self._data = {} # Our internal representation
self._favorites = {} # Our internal representation

@staticmethod
def is_activated():
Expand All @@ -33,100 +35,166 @@ def refresh(self, ttl=None):
"""Get a cached copy or a newer favorites from VRT, or fall back to a cached file"""
if not self.is_activated():
return
favorites_json = get_cache('favorites.json', ttl)
if not favorites_json:
from tokenresolver import TokenResolver
xvrttoken = TokenResolver().get_token('X-VRT-Token', variant='user')
if xvrttoken:
headers = {
'authorization': 'Bearer ' + xvrttoken,
'content-type': 'application/json',
'Referer': 'https://www.vrt.be/vrtnu',
}
favorites_url = 'https://video-user-data.vrt.be/favorites'
favorites_json = get_url_json(url=favorites_url, cache='favorites.json', headers=headers)
if favorites_json is not None:
self._data = favorites_json

def update(self, program, title, value=True):
favorites_dict = get_cache(self.FAVORITES_CACHE_FILE, ttl)
if not favorites_dict:
favorites_dict = self._generate_favorites_dict(self.get_favorites())
if favorites_dict is not None:
from json import dumps
self._favorites = favorites_dict
update_cache(self.FAVORITES_CACHE_FILE, dumps(self._favorites))

def update(self, program_name, title, program_id, is_favorite=True):
"""Set a program as favorite, and update local copy"""

# Survive any recent updates
self.refresh(ttl=5)

if value is self.is_favorite(program):
if is_favorite is self.is_favorite(program_name):
# Already followed/unfollowed, nothing to do
return True

from tokenresolver import TokenResolver
xvrttoken = TokenResolver().get_token('X-VRT-Token', variant='user')
if xvrttoken is None:
log_error('Failed to get favorites token from VRT NU')
notification(message=localize(30975))
return False

headers = {
'authorization': 'Bearer ' + xvrttoken,
'content-type': 'application/json',
'Referer': 'https://www.vrt.be/vrtnu',
}
# Lookup program_id
if program_id == 'None':
program_id = self.get_program_id_graphql(program_name)

# Update local favorites cache
if is_favorite is True:
self._favorites[program_name] = dict(
program_id=program_id,
title=title)
else:
del self._favorites[program_name]

# Update cache dict
from json import dumps
from utils import program_to_url
payload = dict(isFavorite=value, programUrl=program_to_url(program, 'short'), title=title)
data = dumps(payload).encode('utf-8')
program_id = program_to_id(program)
try:
get_url_json('https://video-user-data.vrt.be/favorites/{program_id}'.format(program_id=program_id), headers=headers, data=data, raise_errors='all')
except HTTPError as exc:
log_error("Failed to (un)follow program '{program}' at VRT NU ({error})", program=program, error=exc)
notification(message=localize(30976, program=program))
return False
# NOTE: Updates to favorites take a longer time to take effect, so we keep our own cache and use it
self._data[program_id] = dict(value=payload)
update_cache('favorites.json', dumps(self._data))
update_cache(self.FAVORITES_CACHE_FILE, dumps(self._favorites))
invalidate_caches('my-offline-*.json', 'my-recent-*.json')

# Update online
self.set_favorite_graphql(program_id, title, is_favorite)
return True

def is_favorite(self, program):
def get_favorites(self):
"""Get favorites using VRT NU REST API"""
from tokenresolver import TokenResolver
vrtlogin_at = TokenResolver().get_token('vrtlogin-at')
favorites_json = {}
if vrtlogin_at:
headers = {
'Authorization': 'Bearer ' + vrtlogin_at,
'Accept': 'application/json',
}
querystring = 'tileType=program-poster&tileContentType=program&tileOrientation=portrait&layout=slider&title=Mijn+favoriete+programma%27s'
favorites_json = get_url_json(url='{}?{}'.format(self.FAVORITES_REST_URL, querystring), cache=None, headers=headers, raise_errors='all')
return favorites_json

def get_program_id_graphql(self, program_name):
"""Get programId from programName using GraphQL API"""
from tokenresolver import TokenResolver
vrtlogin_at = TokenResolver().get_token('vrtlogin-at')
program_id = None
if vrtlogin_at:
headers = {
'Authorization': 'Bearer ' + vrtlogin_at,
'Content-Type': 'application/json',
}
graphql = """
query Page($id: ID!) {
page(id: $id) {
... on IPage {
id
}
}
}
"""
payload = dict(
variables=dict(
id='/vrtnu/a-z/{}.model.json'.format(program_name)
),
query=graphql,
)
from json import dumps
data = dumps(payload).encode('utf-8')
page_json = get_url_json(url=self.GRAPHQL_URL, cache=None, headers=headers, data=data, raise_errors='all')
program_id = page_json.get('data').get('page').get('id')
return program_id

def set_favorite_graphql(self, program_id, title, is_favorite=True):
"""Set favorite using GraphQL API"""
from tokenresolver import TokenResolver
vrtlogin_at = TokenResolver().get_token('vrtlogin-at')
result_json = {}
if vrtlogin_at:
headers = {
'Authorization': 'Bearer ' + vrtlogin_at,
'Content-Type': 'application/json',
}
graphql_query = """
mutation setFavorite($input: FavoriteActionInput!) {
setFavorite(input: $input) {
id
favorite
}
}
"""
payload = dict(
variables=dict(
input=dict(
id=program_id,
title=title,
favorite=is_favorite,
),
),
query=graphql_query,
)
from json import dumps
data = dumps(payload).encode('utf-8')
result_json = get_url_json(url=self.GRAPHQL_URL, cache=None, headers=headers, data=data, raise_errors='all')
return result_json

def is_favorite(self, program_name):
"""Is a program a favorite ?"""
value = False
favorite = self._data.get(program_to_id(program))
if favorite:
value = favorite.get('value', {}).get('isFavorite')
return value is True
return program_name in self._favorites

def follow(self, program, title):
def follow(self, program_name, title, program_id=None):
"""Follow your favorite program"""
succeeded = self.update(program, title, True)
succeeded = self.update(program_name, title, program_id, True)
if succeeded:
notification(message=localize(30411, title=title))
container_refresh()

def unfollow(self, program, title, move_down=False):
def unfollow(self, program_name, title, program_id=None, move_down=False):
"""Unfollow your favorite program"""
succeeded = self.update(program, title, False)
succeeded = self.update(program_name, title, program_id, False)
if succeeded:
notification(message=localize(30412, title=title))
# If the current item is selected and we need to move down before removing
if move_down:
input_down()
container_refresh()

def titles(self):
"""Return all favorite titles"""
return [value.get('value').get('title') for value in list(self._data.values()) if value.get('value').get('isFavorite')]

def programs(self):
"""Return all favorite programs"""
from utils import url_to_program
return [url_to_program(value.get('value').get('programUrl')) for value in list(self._data.values()) if value.get('value').get('isFavorite')]
return self._favorites.keys()

@staticmethod
def _generate_favorites_dict(favorites_json):
"""Generate a simple favorites dict with programIds and programNames"""
favorites_dict = {}
for item in favorites_json.get(':items', []):
program_name = favorites_json.get(':items')[item].get('data').get('program').get('name')
program_id = favorites_json.get(':items')[item].get('data').get('program').get('id')
title = favorites_json.get(':items')[item].get('title')
favorites_dict[program_name] = dict(
program_id=program_id,
title=title)
return favorites_dict

def manage(self):
"""Allow the user to unselect favorites to be removed from the listing"""
from utils import url_to_program
self.refresh(ttl=0)
if not self._data:
if not self._favorites:
ok_dialog(heading=localize(30418), message=localize(30419)) # No favorites found
return

Expand All @@ -136,12 +204,12 @@ def by_title(item):

items = [dict(program=url_to_program(value.get('value').get('programUrl')),
title=unquote(value.get('value').get('title')),
enabled=value.get('value').get('isFavorite')) for value in list(sorted(list(self._data.values()), key=by_title))]
enabled=value.get('value').get('isFavorite')) for value in list(sorted(list(self._favorites.values()), key=by_title))]
titles = [item['title'] for item in items]
preselect = [idx for idx in range(0, len(items) - 1) if items[idx]['enabled']]
selected = multiselect(localize(30420), options=titles, preselect=preselect) # Please select/unselect to follow/unfollow
if selected is not None:
for idx in set(preselect).difference(set(selected)):
self.unfollow(program=items[idx]['program'], title=items[idx]['title'])
self.unfollow(program_id=None, program_name=items[idx]['program'], title=items[idx]['title'])
for idx in set(selected).difference(set(preselect)):
self.follow(program=items[idx]['program'], title=items[idx]['title'])
self.follow(program_id=None, program_name=items[idx]['program'], title=items[idx]['title'])
18 changes: 11 additions & 7 deletions resources/lib/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def get_studio(api_data):
# Not Found
return ''

def get_context_menu(self, api_data, program, cache_file):
def get_context_menu(self, api_data, program_name, cache_file):
"""Get context menu"""
from addon import plugin
favorite_marker = ''
Expand Down Expand Up @@ -98,48 +98,52 @@ def get_context_menu(self, api_data, program, cache_file):

# VRT NU Search API
if api_data.get('type') == 'episode':
program_id = api_data.get('programId')
program_title = api_data.get('program')
program_type = api_data.get('programType')
follow_suffix = localize(30410) if program_type != 'oneoff' else '' # program
follow_enabled = True

# VRT NU Suggest API
elif api_data.get('type') == 'program':
# FIXME: No program_id in Suggest API
program_id = None
program_title = api_data.get('title')
follow_suffix = ''
follow_enabled = True

# VRT NU Schedule API (some are missing vrt.whatson-id)
elif api_data.get('vrt.whatson-id') or api_data.get('startTime'):
program_id = api_data.get('programId')
program_title = api_data.get('title')
follow_suffix = localize(30410) # program
follow_enabled = bool(api_data.get('url'))

if follow_enabled and program:
if follow_enabled and program_name:
program_title = to_unicode(quote_plus(from_unicode(program_title))) # We need to ensure forward slashes are quoted
if self._favorites.is_favorite(program):
if self._favorites.is_favorite(program_name):
extras = {}
# If we are in a favorites menu, move cursor down before removing a favorite
if plugin.path.startswith('/favorites'):
extras = dict(move_down=True)
context_menu.append((
localize(30412, title=follow_suffix), # Unfollow
'RunPlugin(%s)' % url_for('unfollow', program=program, title=program_title, **extras)
'RunPlugin(%s)' % url_for('unfollow', program_name=program_name, title=program_title, program_id=program_id, **extras)
))
favorite_marker = '[COLOR={highlighted}]ᵛ[/COLOR]'
else:
context_menu.append((
localize(30411, title=follow_suffix), # Follow
'RunPlugin(%s)' % url_for('follow', program=program, title=program_title)
'RunPlugin(%s)' % url_for('follow', program_name=program_name, title=program_title, program_id=program_id)
))

# GO TO PROGRAM
if api_data.get('programType') != 'oneoff' and program:
if api_data.get('programType') != 'oneoff' and program_name:
if plugin.path.startswith(('/favorites/offline', '/favorites/recent', '/offline', '/recent',
'/resumepoints/continue', '/resumepoints/watchlater', '/tvguide')):
context_menu.append((
localize(30417), # Go to program
'Container.Update(%s)' % url_for('programs', program=program, season='allseasons')
'Container.Update(%s)' % url_for('programs', program_name=program_name, season='allseasons')
))

# REFRESH MENU
Expand Down
4 changes: 2 additions & 2 deletions resources/lib/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,9 @@ def video_to_api_url(url):
return url


def program_to_id(program):
def program_to_str(program):
"""Convert a program url component (e.g. de-campus-cup)
to a favorite program_id (e.g. vrtnuazdecampuscup), used for lookups in favorites dict"""
to a favorite program_str (e.g. vrtnuazdecampuscup), used for lookups in favorites dict"""
return 'vrtnuaz' + program.replace('-', '')


Expand Down
2 changes: 1 addition & 1 deletion resources/lib/vrtplayer.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ def show_favorites_menu(self):
show_listing(favorites_items, category=30010, cache=False) # My favorites

# Show dialog when no favorites were found
if not self._favorites.titles():
if not self._favorites.programs():
ok_dialog(heading=localize(30415), message=localize(30416))

def show_favorites_docu_menu(self):
Expand Down
Loading

0 comments on commit 2b5c162

Please sign in to comment.