From 7f8b816c05f333ed54918d998fb947a4358317f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Sun, 20 Oct 2019 13:35:37 +0200 Subject: [PATCH 1/7] Implement continue watching menu --- .../resource.language.en_gb/strings.po | 8 + .../resource.language.nl_nl/strings.po | 8 + resources/lib/plugin.py | 117 +++++++++++--- resources/lib/service.py | 4 +- resources/lib/vtmgo/vtmgo.py | 145 ++++++++++-------- test/test_vtmgo.py | 2 +- test/userdata/credentials.json.example | 3 +- 7 files changed, 202 insertions(+), 85 deletions(-) diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index 699d8ea1..80359a42 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -78,6 +78,14 @@ msgctxt "#30018" msgid "Show your personal favourites" msgstr "" +msgctxt "#30019" +msgid "Continue watching" +msgstr "" + +msgctxt "#30020" +msgid "Continue watching where you left off" +msgstr "" + ### CONTEXT MENU diff --git a/resources/language/resource.language.nl_nl/strings.po b/resources/language/resource.language.nl_nl/strings.po index 3748a9e3..b707307d 100644 --- a/resources/language/resource.language.nl_nl/strings.po +++ b/resources/language/resource.language.nl_nl/strings.po @@ -79,6 +79,14 @@ msgctxt "#30018" msgid "Show your personal favourites" msgstr "Bekijk je persoonlijke favorieten" +msgctxt "#30019" +msgid "Continue watching" +msgstr "Verder kijken" + +msgctxt "#30020" +msgid "Continue watching where you left off" +msgstr "Kijk verder waar je gebleven was" + ### CONTEXT MENU diff --git a/resources/lib/plugin.py b/resources/lib/plugin.py index 5e39e899..478af84e 100644 --- a/resources/lib/plugin.py +++ b/resources/lib/plugin.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- """ Addon code """ +# pylint: disable=too-many-lines from __future__ import absolute_import, division, unicode_literals @@ -7,7 +8,7 @@ from resources.lib import GeoblockedException, UnavailableException from resources.lib.kodiwrapper import KodiWrapper, TitleItem -from resources.lib.vtmgo.vtmgo import Content, VtmGo +from resources.lib.vtmgo.vtmgo import Content, VtmGo, Episode from resources.lib.vtmgo.vtmgoauth import VtmGoAuth, InvalidLoginException from resources.lib.vtmgo.vtmgoepg import VtmGoEpg from resources.lib.vtmgo.vtmgostream import VtmGoStream @@ -78,6 +79,14 @@ def show_index(): info_dict={ 'plot': kodi.localize(30018), }), + TitleItem(title=kodi.localize(30019), # Continue watching + path=routing.url_for(show_continuewatching if not kids else show_kids_continuewatching), + art_dict={ + 'icon': 'DefaultInProgressShows.png' + }, + info_dict={ + 'plot': kodi.localize(30020), + }), ]) # Only provide YouTube option when plugin.video.youtube is available @@ -433,7 +442,7 @@ def show_kids_mylist(): def show_mylist(): """ Show the items in "My List" """ try: - mylist = vtm_go.get_mylist() + mylist = vtm_go.get_swimlane('my-list') except Exception as ex: kodi.show_notification(message=str(ex)) raise @@ -472,6 +481,29 @@ def mylist_del(video_type, content_id): kodi.end_of_directory() +@routing.route('/kids/continuewatching') +def show_kids_continuewatching(): + """ Show the items in "Continue Watching" (kids) """ + show_continuewatching() + + +@routing.route('/continuewatching') +def show_continuewatching(): + """ Show the items in "Continue Watching" """ + try: + mylist = vtm_go.get_swimlane('continue-watching') + except Exception as ex: + kodi.show_notification(message=str(ex)) + raise + + listing = [] + for item in mylist: + listing.append(_generate_titleitem(item, True)) + + # Sort categories by default like in VTM GO. + kodi.show_listing(listing, 30019, content='episodes', sort='label') + + @routing.route('/kids/catalog') def show_kids_catalog(): """ Show the catalog (kids) """ @@ -732,24 +764,25 @@ def _generate_titleitem(item, my_list=False): 'thumb': item.cover, } info_dict = { - 'title': item.title, + 'title': item.name, 'plot': item.description, } - if my_list: - context_menu = [( - kodi.localize(30051), # Remove from My List - 'XBMC.Container.Update(%s)' % - routing.url_for(mylist_del if not kids else kids_mylist_del, video_type=item.video_type, content_id=item.content_id) - )] - else: - context_menu = [( - kodi.localize(30050), # Add to My List - 'XBMC.Container.Update(%s)' % - routing.url_for(mylist_add if not kids else kids_mylist_add, video_type=item.video_type, content_id=item.content_id) - )] + if isinstance(item, Content): + if my_list: + context_menu = [( + kodi.localize(30051), # Remove from My List + 'XBMC.Container.Update(%s)' % + routing.url_for(mylist_del if not kids else kids_mylist_del, video_type=item.video_type, content_id=item.content_id) + )] + else: + context_menu = [( + kodi.localize(30050), # Add to My List + 'XBMC.Container.Update(%s)' % + routing.url_for(mylist_add if not kids else kids_mylist_add, video_type=item.video_type, content_id=item.content_id) + )] - if item.video_type == Content.CONTENT_TYPE_MOVIE: + if isinstance(item, Content) and item.video_type == Content.CONTENT_TYPE_MOVIE: # Get movie details from cache movie = vtm_go.get_movie(item.content_id, only_cache=True) if movie: @@ -768,7 +801,7 @@ def _generate_titleitem(item, my_list=False): 'mediatype': 'movie', }) - return TitleItem(title=item.title, + return TitleItem(title=item.name, path=routing.url_for(play, category='movies', item=item.content_id), art_dict=art_dict, info_dict=info_dict, @@ -780,7 +813,7 @@ def _generate_titleitem(item, my_list=False): context_menu=context_menu, is_playable=True) - if item.video_type == Content.CONTENT_TYPE_PROGRAM: + if isinstance(item, Content) and item.video_type == Content.CONTENT_TYPE_PROGRAM: # Get program details from cache program = vtm_go.get_program(item.content_id, only_cache=True) if program: @@ -799,13 +832,57 @@ def _generate_titleitem(item, my_list=False): 'mediatype': None, }) - return TitleItem(title=item.title, + return TitleItem(title=item.name, path=routing.url_for(show_program, program=item.content_id), art_dict=art_dict, info_dict=info_dict, context_menu=context_menu) - return None + if isinstance(item, Episode): + title = '%dx%02d. %s' % (item.season, item.number, item.name) + info_dict.update({ + 'mediatype': 'episode', + 'season': item.season, + 'episode': item.number, + }) + stream_dict = { + 'codec': 'h264', + 'height': 1080, + 'width': 1920, + } + + # Get program details + program = vtm_go.get_program(item.program_id, only_cache=True) + episode = vtm_go.get_episode_from_program(item.program_id, item.episode_id, only_cache=True) + if program and episode: + title = '%dx%02d. %s - %s' % (item.season, item.number, program.name, episode.name) + art_dict.update({ + 'fanart': episode.cover, + 'banner': episode.cover, + }) + info_dict.update({ + 'title': title, + 'tvshowtitle': program.name, + 'tagline': program.description, + 'plot': _format_plot(episode), + 'duration': episode.duration, + 'set': program.name, + 'studio': episode.channel, + 'aired': episode.aired, + 'mpaa': ', '.join(episode.legal) if hasattr(episode, 'legal') and episode.legal else kodi.localize(30216), + }) + stream_dict.update({ + 'duration': episode.duration, + }) + + return TitleItem(title=title, + path=routing.url_for(play, category='episodes', item=item.episode_id), + art_dict=art_dict, + info_dict=info_dict, + stream_dict=stream_dict, + is_playable=True) + + raise Exception('Unknown video_type') @routing.route('/play/epg//') diff --git a/resources/lib/service.py b/resources/lib/service.py index 49edbf79..a0984c9b 100644 --- a/resources/lib/service.py +++ b/resources/lib/service.py @@ -28,7 +28,7 @@ def run(self): # Update every `update_interval` after the last update if self.kodi.get_setting_as_bool('metadata_update') \ and int(self.kodi.get_setting('metadata_last_updated', 0)) + self.update_interval < time(): - self.update_metadata() + self._update_metadata() self.kodi.set_setting('metadata_last_updated', str(int(time()))) # Stop when abort requested @@ -44,7 +44,7 @@ def onSettingsChanged(self): # Refresh our VtmGo instance self.vtm_go = VtmGo(self.kodi) - def update_metadata(self, delay=10): + def _update_metadata(self, delay=10): """ Update the metadata for the listings. """ self.kodi.log('Updating metadata in the background') diff --git a/resources/lib/vtmgo/vtmgo.py b/resources/lib/vtmgo/vtmgo.py index f0a76b1b..aa6c51ec 100644 --- a/resources/lib/vtmgo/vtmgo.py +++ b/resources/lib/vtmgo/vtmgo.py @@ -74,14 +74,15 @@ def __repr__(self): class Content: - """ Defines an item from the catalogue""" + """ Defines an item from the catalogue """ CONTENT_TYPE_MOVIE = 'MOVIE' CONTENT_TYPE_PROGRAM = 'PROGRAM' + CONTENT_TYPE_EPISODE = 'EPISODE' - def __init__(self, content_id=None, title=None, description=None, cover=None, video_type=None, my_list=False, geoblocked=None): + def __init__(self, content_id=None, name=None, description=None, cover=None, video_type=None, my_list=False, geoblocked=None): """ :type content_id: str - :type title: str + :type name: str :type description: str :type cover: str :type video_type: str @@ -89,7 +90,7 @@ def __init__(self, content_id=None, title=None, description=None, cover=None, vi :type geoblocked: bool """ self.content_id = content_id - self.title = title + self.name = name self.description = description if description else '' self.cover = cover self.video_type = video_type @@ -101,7 +102,7 @@ def __repr__(self): class Movie: - """ Defines a Movie""" + """ Defines a Movie """ def __init__(self, movie_id=None, name=None, description=None, year=None, cover=None, duration=None, remaining=None, geoblocked=None, channel=None, legal=None, aired=None): @@ -135,7 +136,7 @@ def __repr__(self): class Program: - """ Defines a Program""" + """ Defines a Program """ def __init__(self, program_id=None, name=None, description=None, cover=None, seasons=None, geoblocked=None, channel=None, legal=None): """ @@ -162,7 +163,7 @@ def __repr__(self): class Season: - """ Defines a Season""" + """ Defines a Season """ def __init__(self, number=None, episodes=None, cover=None, geoblocked=None, channel=None, legal=None): """ @@ -187,10 +188,12 @@ def __repr__(self): class Episode: """ Defines an Episode """ - def __init__(self, episode_id=None, number=None, season=None, name=None, description=None, cover=None, duration=None, remaining=None, geoblocked=None, + def __init__(self, episode_id=None, program_id=None, number=None, season=None, name=None, description=None, cover=None, duration=None, remaining=None, + geoblocked=None, channel=None, legal=None, aired=None): """ :type episode_id: str + :type program_id: str :type number: int :type season: str :type name: str @@ -205,6 +208,7 @@ def __init__(self, episode_id=None, number=None, season=None, name=None, descrip """ import re self.episode_id = episode_id + self.program_id = program_id self.number = int(number) self.season = int(season) self.name = re.compile('^%d. ' % number).sub('', name) # Strip episode from name @@ -223,6 +227,13 @@ def __repr__(self): class VtmGo: """ VTM GO API """ + _headers = { + 'x-app-version': '5', + 'x-persgroep-mobile-app': 'true', + 'x-persgroep-os': 'android', + 'x-persgroep-os-version': '23', + 'User-Agent': 'VTMGO/6.8.2 (be.vmma.vtm.zenderapp; build:11215; Android 23) okhttp/3.14.2' + } def __init__(self, kodi): self._kodi = kodi # type: KodiWrapper @@ -249,31 +260,33 @@ def get_recommendations(self): categories = [] for cat in recommendations.get('rows', []): - if cat.get('rowType') in ['SWIMLANE_DEFAULT']: - items = [] - - for item in cat.get('teasers'): - items.append(Content( - content_id=item.get('target', {}).get('id'), - video_type=item.get('target', {}).get('type'), - title=item.get('title'), - geoblocked=item.get('geoBlocked'), - cover=item.get('imageUrl'), - )) - - categories.append(Category( - category_id=cat.get('id'), - title=cat.get('title'), - content=items, + if cat.get('rowType') not in ['SWIMLANE_DEFAULT']: + self._kodi.log('Skipping recommendation {name} with type={type}', name=cat.get('title'), type=cat.get('rowType')) + continue + + items = [] + for item in cat.get('teasers'): + items.append(Content( + content_id=item.get('target', {}).get('id'), + video_type=item.get('target', {}).get('type'), + name=item.get('title'), + geoblocked=item.get('geoBlocked'), + cover=item.get('imageUrl'), )) + categories.append(Category( + category_id=cat.get('id'), + title=cat.get('title'), + content=items, + )) + return categories - def get_mylist(self): + def get_swimlane(self, swimlane=None): """ Returns the contents of My List """ - response = self._get_url('/%s/main/swimlane/my-list' % self._mode()) + response = self._get_url('/%s/main/swimlane/%s' % (self._mode(), swimlane)) - # My list can be empty + # Result can be empty if not response: return [] @@ -281,13 +294,24 @@ def get_mylist(self): items = [] for item in result.get('teasers'): - items.append(Content( - content_id=item.get('target', {}).get('id'), - video_type=item.get('target', {}).get('type'), - title=item.get('title'), - geoblocked=item.get('geoBlocked'), - cover=item.get('imageUrl'), - )) + if item.get('target', {}).get('type') == Content.CONTENT_TYPE_EPISODE: + items.append(Episode( + episode_id=item.get('target', {}).get('id'), + program_id=item.get('target', {}).get('programId'), + number=item.get('target', {}).get('episodeIndex'), + season=item.get('target', {}).get('seasonIndex'), + name='%dx%02d. ' % (item.get('target', {}).get('episodeIndex'), item.get('target', {}).get('seasonIndex')) + item.get('title'), + geoblocked=item.get('geoBlocked'), + cover=item.get('imageUrl'), + )) + else: + items.append(Content( + content_id=item.get('target', {}).get('id'), + video_type=item.get('target', {}).get('type'), + name=item.get('title'), + geoblocked=item.get('geoBlocked'), + cover=item.get('imageUrl'), + )) return items @@ -356,7 +380,7 @@ def get_items(self, category=None): for item in info.get('pagedTeasers', {}).get('content', []): items.append(Content( content_id=item.get('target', {}).get('id'), - title=item.get('title'), + name=item.get('title'), cover=item.get('imageUrl'), video_type=item.get('target', {}).get('type'), geoblocked=item.get('geoBlocked'), @@ -435,6 +459,7 @@ def get_program(self, program_id, only_cache=False): for item_episode in item_season.get('episodes', []): episodes[item_episode.get('index')] = Episode( episode_id=item_episode.get('id'), + program_id=program_id, number=item_episode.get('index'), season=item_season.get('index'), name=item_episode.get('name'), @@ -468,11 +493,30 @@ def get_program(self, program_id, only_cache=False): legal=program.get('legalIcons'), ) + def get_episode_from_program(self, program_id, episode_id, only_cache=False): + """ Get the details of the specified episode. + :type program_id: str + :type episode_id: str + :type only_cache: bool + :rtype Episode + """ + program = self.get_program(program_id, only_cache=only_cache) + if not program: + raise UnavailableException() + + for season in program.seasons.values(): + for episode in season.episodes.values(): + if episode.episode_id == episode_id: + return episode + + raise UnavailableException() + def get_episode(self, episode_id): """ Get the details of the specified episode. :type episode_id: str :rtype Episode """ + # The following API doesn't seem to be available in API version 6 anymore. response = self._get_url('/%s/episodes/%s' % (self._mode(), episode_id)) info = json.loads(response) @@ -499,7 +543,7 @@ def do_search(self, search): for item in results.get('suggestions', []): items.append(Content( content_id=item.get('id'), - title=item.get('name'), + name=item.get('name'), video_type=item.get('type'), )) @@ -510,14 +554,7 @@ def _get_url(self, url): :type url: str :rtype str """ - headers = { - 'x-app-version': '5', - 'x-persgroep-mobile-app': 'true', - 'x-persgroep-os': 'android', - 'x-persgroep-os-version': '23', - 'User-Agent': 'VTMGO/6.5.0 (be.vmma.vtm.zenderapp; build:11019; Android 23) okhttp/3.12.1' - } - + headers = self._headers token = self._auth.get_token() if token: headers['x-dpp-jwt'] = token @@ -526,7 +563,7 @@ def _get_url(self, url): response = requests.session().get('https://api.vtmgo.be' + url, headers=headers, verify=False, proxies=self._proxies) - self._kodi.log('Got response: {response}', LOG_DEBUG, response=response.text) + self._kodi.log('Got response (status={code}): {response}', LOG_DEBUG, code=response.status_code, response=response.text) if response.status_code == 404: raise UnavailableException() @@ -541,14 +578,7 @@ def _put_url(self, url): :type url: str :rtype str """ - headers = { - 'x-app-version': '5', - 'x-persgroep-mobile-app': 'true', - 'x-persgroep-os': 'android', - 'x-persgroep-os-version': '23', - 'User-Agent': 'VTMGO/6.5.0 (be.vmma.vtm.zenderapp; build:11019; Android 23) okhttp/3.12.1' - } - + headers = self._headers token = self._auth.get_token() if token: headers['x-dpp-jwt'] = token @@ -572,14 +602,7 @@ def _delete_url(self, url): :type url: str :rtype str """ - headers = { - 'x-app-version': '5', - 'x-persgroep-mobile-app': 'true', - 'x-persgroep-os': 'android', - 'x-persgroep-os-version': '23', - 'User-Agent': 'VTMGO/6.5.0 (be.vmma.vtm.zenderapp; build:11019; Android 23) okhttp/3.12.1' - } - + headers = self._headers token = self._auth.get_token() if token: headers['x-dpp-jwt'] = token diff --git a/test/test_vtmgo.py b/test/test_vtmgo.py index e66ea076..57d1b755 100644 --- a/test/test_vtmgo.py +++ b/test/test_vtmgo.py @@ -48,7 +48,7 @@ def test_get_recommendations(self): # print(main) def test_get_mylist(self): - mylist = self._vtmgo.get_mylist() + mylist = self._vtmgo.get_swimlane('my-list') self.assertIsInstance(mylist, list) # print(mylist) diff --git a/test/userdata/credentials.json.example b/test/userdata/credentials.json.example index 4d57c0ea..3b38b842 100644 --- a/test/userdata/credentials.json.example +++ b/test/userdata/credentials.json.example @@ -1,4 +1,5 @@ { "username": "username", - "password": "password" + "password": "password", + "credentials_hash": "ffffffffffffffffffffffffffffffff" } \ No newline at end of file From a71a3ca6eb3fb027fc18a44c8ef65ef2fafd885d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Sun, 20 Oct 2019 14:46:26 +0200 Subject: [PATCH 2/7] Refactor out Content. --- .../resource.language.en_gb/strings.po | 4 + .../resource.language.nl_nl/strings.po | 4 + resources/lib/plugin.py | 80 ++++++----- resources/lib/service.py | 17 ++- resources/lib/vtmgo/vtmgo.py | 129 +++++++++--------- 5 files changed, 131 insertions(+), 103 deletions(-) diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index 80359a42..9da57f25 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -97,6 +97,10 @@ msgctxt "#30051" msgid "Remove from My List" msgstr "" +msgctxt "#30052" +msgid "Go to Program" +msgstr "" + ### CODE msgctxt "#30200" diff --git a/resources/language/resource.language.nl_nl/strings.po b/resources/language/resource.language.nl_nl/strings.po index b707307d..80d66a35 100644 --- a/resources/language/resource.language.nl_nl/strings.po +++ b/resources/language/resource.language.nl_nl/strings.po @@ -98,6 +98,10 @@ msgctxt "#30051" msgid "Remove from My List" msgstr "Verwijderen uit mijn lijst" +msgctxt "#30052" +msgid "Go to Program" +msgstr "Ga naar programma" + ### CODE msgctxt "#30200" diff --git a/resources/lib/plugin.py b/resources/lib/plugin.py index 478af84e..87f9e2cc 100644 --- a/resources/lib/plugin.py +++ b/resources/lib/plugin.py @@ -8,7 +8,7 @@ from resources.lib import GeoblockedException, UnavailableException from resources.lib.kodiwrapper import KodiWrapper, TitleItem -from resources.lib.vtmgo.vtmgo import Content, VtmGo, Episode +from resources.lib.vtmgo.vtmgo import VtmGo, Episode, Movie, Program from resources.lib.vtmgo.vtmgoauth import VtmGoAuth, InvalidLoginException from resources.lib.vtmgo.vtmgoepg import VtmGoEpg from resources.lib.vtmgo.vtmgostream import VtmGoStream @@ -157,13 +157,13 @@ def metadata_update(delay=10): # Loop over all of them and download the metadata for index, item in enumerate(items): # Update the items - if item.video_type == Content.CONTENT_TYPE_MOVIE: - if not vtm_go.get_movie(item.content_id, only_cache=True): - vtm_go.get_movie(item.content_id) + if isinstance(item, Movie): + if not vtm_go.get_movie(item.movie_id, only_cache=True): + vtm_go.get_movie(item.movie_id) xbmc.sleep(delay) - elif item.video_type == Content.CONTENT_TYPE_PROGRAM: - if not vtm_go.get_program(item.content_id, only_cache=True): - vtm_go.get_program(item.content_id) + elif isinstance(item, Program): + if not vtm_go.get_program(item.program_id, only_cache=True): + vtm_go.get_program(item.program_id) xbmc.sleep(delay) # Upgrade the progress bar @@ -754,8 +754,8 @@ def show_search(query=None): def _generate_titleitem(item, my_list=False): - """ Generate a TitleItem based on a Content. - :type item: Content + """ Generate a TitleItem based on a Movie, Program or Episode. + :type item: Union[Movie, Program, Episode] :rtype TitleItem """ kids = kodi.kids_mode() @@ -768,23 +768,26 @@ def _generate_titleitem(item, my_list=False): 'plot': item.description, } - if isinstance(item, Content): + if isinstance(item, Movie): if my_list: context_menu = [( kodi.localize(30051), # Remove from My List 'XBMC.Container.Update(%s)' % - routing.url_for(mylist_del if not kids else kids_mylist_del, video_type=item.video_type, content_id=item.content_id) + routing.url_for(mylist_del if not kids else kids_mylist_del, video_type=vtm_go.CONTENT_TYPE_MOVIE, content_id=item.movie_id) )] else: context_menu = [( kodi.localize(30050), # Add to My List 'XBMC.Container.Update(%s)' % - routing.url_for(mylist_add if not kids else kids_mylist_add, video_type=item.video_type, content_id=item.content_id) + routing.url_for(mylist_add if not kids else kids_mylist_add, video_type=vtm_go.CONTENT_TYPE_MOVIE, content_id=item.movie_id) )] - if isinstance(item, Content) and item.video_type == Content.CONTENT_TYPE_MOVIE: + info_dict.update({ + 'mediatype': 'movie', + }) + # Get movie details from cache - movie = vtm_go.get_movie(item.content_id, only_cache=True) + movie = vtm_go.get_movie(item.movie_id, only_cache=True) if movie: art_dict.update({ 'fanart': movie.cover, @@ -797,12 +800,8 @@ def _generate_titleitem(item, my_list=False): 'mpaa': ', '.join(movie.legal) if hasattr(movie, 'legal') and movie.legal else kodi.localize(30216), }) - info_dict.update({ - 'mediatype': 'movie', - }) - return TitleItem(title=item.name, - path=routing.url_for(play, category='movies', item=item.content_id), + path=routing.url_for(play, category='movies', item=item.movie_id), art_dict=art_dict, info_dict=info_dict, stream_dict={ @@ -813,9 +812,26 @@ def _generate_titleitem(item, my_list=False): context_menu=context_menu, is_playable=True) - if isinstance(item, Content) and item.video_type == Content.CONTENT_TYPE_PROGRAM: + if isinstance(item, Program): + if my_list: + context_menu = [( + kodi.localize(30051), # Remove from My List + 'XBMC.Container.Update(%s)' % + routing.url_for(mylist_del if not kids else kids_mylist_del, video_type=vtm_go.CONTENT_TYPE_PROGRAM, content_id=item.program_id) + )] + else: + context_menu = [( + kodi.localize(30050), # Add to My List + 'XBMC.Container.Update(%s)' % + routing.url_for(mylist_add if not kids else kids_mylist_add, video_type=vtm_go.CONTENT_TYPE_PROGRAM, content_id=item.program_id) + )] + + info_dict.update({ + 'mediatype': None, + }) + # Get program details from cache - program = vtm_go.get_program(item.content_id, only_cache=True) + program = vtm_go.get_program(item.program_id, only_cache=True) if program: art_dict.update({ 'fanart': program.cover, @@ -828,19 +844,21 @@ def _generate_titleitem(item, my_list=False): 'season': len(program.seasons), }) - info_dict.update({ - 'mediatype': None, - }) - return TitleItem(title=item.name, - path=routing.url_for(show_program, program=item.content_id), + path=routing.url_for(show_program, program=item.program_id), art_dict=art_dict, info_dict=info_dict, context_menu=context_menu) if isinstance(item, Episode): - title = '%dx%02d. %s' % (item.season, item.number, item.name) + context_menu = [( + kodi.localize(30052), # Go to Program + 'XBMC.Container.Update(%s)' % + routing.url_for(show_program, program=item.program_id) + )] + info_dict.update({ + 'title': '%dx%02d. %s' % (item.season, item.number, item.name), 'mediatype': 'episode', 'season': item.season, 'episode': item.number, @@ -855,13 +873,12 @@ def _generate_titleitem(item, my_list=False): program = vtm_go.get_program(item.program_id, only_cache=True) episode = vtm_go.get_episode_from_program(item.program_id, item.episode_id, only_cache=True) if program and episode: - title = '%dx%02d. %s - %s' % (item.season, item.number, program.name, episode.name) art_dict.update({ 'fanart': episode.cover, 'banner': episode.cover, }) info_dict.update({ - 'title': title, + 'title': '%dx%02d. %s - %s' % (item.season, item.number, program.name, episode.name), 'tvshowtitle': program.name, 'tagline': program.description, 'plot': _format_plot(episode), @@ -875,11 +892,12 @@ def _generate_titleitem(item, my_list=False): 'duration': episode.duration, }) - return TitleItem(title=title, + return TitleItem(title=info_dict['title'], path=routing.url_for(play, category='episodes', item=item.episode_id), art_dict=art_dict, info_dict=info_dict, stream_dict=stream_dict, + context_menu=context_menu, is_playable=True) raise Exception('Unknown video_type') @@ -1011,7 +1029,7 @@ def play(category, item): def _format_plot(obj): - """ Format the plot for a Content item """ + """ Format the plot for a item """ plot = '' if hasattr(obj, 'description'): diff --git a/resources/lib/service.py b/resources/lib/service.py index a0984c9b..388827e1 100644 --- a/resources/lib/service.py +++ b/resources/lib/service.py @@ -5,10 +5,9 @@ from time import time -from xbmc import Monitor - from resources.lib.kodiwrapper import KodiWrapper, LOG_INFO -from resources.lib.vtmgo.vtmgo import VtmGo, Content +from resources.lib.vtmgo.vtmgo import VtmGo, Program, Movie +from xbmc import Monitor class BackgroundService(Monitor): @@ -62,13 +61,13 @@ def _update_metadata(self, delay=10): # Loop over all of them and download the metadata for index, item in enumerate(items): # Update the items - if item.video_type == Content.CONTENT_TYPE_MOVIE: - if not vtm_go.get_movie(item.content_id, only_cache=True): - vtm_go.get_movie(item.content_id) + if isinstance(item, Movie): + if not vtm_go.get_movie(item.movie_id, only_cache=True): + vtm_go.get_movie(item.movie_id) self.waitForAbort(delay / 1000) - elif item.video_type == Content.CONTENT_TYPE_PROGRAM: - if not vtm_go.get_program(item.content_id, only_cache=True): - vtm_go.get_program(item.content_id) + elif isinstance(item, Program): + if not vtm_go.get_program(item.program_id, only_cache=True): + vtm_go.get_program(item.program_id) self.waitForAbort(delay / 1000) # Upgrade the progress bar diff --git a/resources/lib/vtmgo/vtmgo.py b/resources/lib/vtmgo/vtmgo.py index aa6c51ec..64d606fe 100644 --- a/resources/lib/vtmgo/vtmgo.py +++ b/resources/lib/vtmgo/vtmgo.py @@ -63,7 +63,7 @@ def __init__(self, category_id=None, title=None, content=None): """ :type category_id: str :type title: str - :type content: list[Content] + :type content: list[Union[Movie, Program, Episode]] """ self.category_id = category_id self.title = title @@ -73,34 +73,6 @@ def __repr__(self): return "%r" % self.__dict__ -class Content: - """ Defines an item from the catalogue """ - CONTENT_TYPE_MOVIE = 'MOVIE' - CONTENT_TYPE_PROGRAM = 'PROGRAM' - CONTENT_TYPE_EPISODE = 'EPISODE' - - def __init__(self, content_id=None, name=None, description=None, cover=None, video_type=None, my_list=False, geoblocked=None): - """ - :type content_id: str - :type name: str - :type description: str - :type cover: str - :type video_type: str - :type my_list: bool - :type geoblocked: bool - """ - self.content_id = content_id - self.name = name - self.description = description if description else '' - self.cover = cover - self.video_type = video_type - self.my_list = my_list - self.geoblocked = geoblocked - - def __repr__(self): - return "%r" % self.__dict__ - - class Movie: """ Defines a Movie """ @@ -227,7 +199,11 @@ def __repr__(self): class VtmGo: """ VTM GO API """ - _headers = { + CONTENT_TYPE_MOVIE = 'MOVIE' + CONTENT_TYPE_PROGRAM = 'PROGRAM' + CONTENT_TYPE_EPISODE = 'EPISODE' + + _HEADERS = { 'x-app-version': '5', 'x-persgroep-mobile-app': 'true', 'x-persgroep-os': 'android', @@ -266,13 +242,20 @@ def get_recommendations(self): items = [] for item in cat.get('teasers'): - items.append(Content( - content_id=item.get('target', {}).get('id'), - video_type=item.get('target', {}).get('type'), - name=item.get('title'), - geoblocked=item.get('geoBlocked'), - cover=item.get('imageUrl'), - )) + if item.get('target', {}).get('type') == self.CONTENT_TYPE_MOVIE: + items.append(Movie( + movie_id=item.get('target', {}).get('id'), + name=item.get('title'), + cover=item.get('imageUrl'), + geoblocked=item.get('geoBlocked'), + )) + elif item.get('target', {}).get('type') == self.CONTENT_TYPE_PROGRAM: + items.append(Program( + program_id=item.get('target', {}).get('id'), + name=item.get('title'), + cover=item.get('imageUrl'), + geoblocked=item.get('geoBlocked'), + )) categories.append(Category( category_id=cat.get('id'), @@ -294,7 +277,23 @@ def get_swimlane(self, swimlane=None): items = [] for item in result.get('teasers'): - if item.get('target', {}).get('type') == Content.CONTENT_TYPE_EPISODE: + if item.get('target', {}).get('type') == self.CONTENT_TYPE_MOVIE: + items.append(Movie( + movie_id=item.get('target', {}).get('id'), + name=item.get('title'), + geoblocked=item.get('geoBlocked'), + cover=item.get('imageUrl'), + )) + + elif item.get('target', {}).get('type') == self.CONTENT_TYPE_PROGRAM: + items.append(Program( + program_id=item.get('target', {}).get('id'), + name=item.get('title'), + geoblocked=item.get('geoBlocked'), + cover=item.get('imageUrl'), + )) + + elif item.get('target', {}).get('type') == self.CONTENT_TYPE_EPISODE: items.append(Episode( episode_id=item.get('target', {}).get('id'), program_id=item.get('target', {}).get('programId'), @@ -304,14 +303,6 @@ def get_swimlane(self, swimlane=None): geoblocked=item.get('geoBlocked'), cover=item.get('imageUrl'), )) - else: - items.append(Content( - content_id=item.get('target', {}).get('id'), - video_type=item.get('target', {}).get('type'), - name=item.get('title'), - geoblocked=item.get('geoBlocked'), - cover=item.get('imageUrl'), - )) return items @@ -368,7 +359,7 @@ def get_categories(self): def get_items(self, category=None): """ Get a list of all the items in a category. :type category: str - :rtype list[Content] + :rtype list[Union[Movie, Program]] """ if category and category != 'all': response = self._get_url('/%s/catalog?pageSize=%d&filter=%s' % (self._mode(), 1000, quote(category))) @@ -378,13 +369,20 @@ def get_items(self, category=None): items = [] for item in info.get('pagedTeasers', {}).get('content', []): - items.append(Content( - content_id=item.get('target', {}).get('id'), - name=item.get('title'), - cover=item.get('imageUrl'), - video_type=item.get('target', {}).get('type'), - geoblocked=item.get('geoBlocked'), - )) + if item.get('target', {}).get('type') == self.CONTENT_TYPE_MOVIE: + items.append(Movie( + movie_id=item.get('target', {}).get('id'), + name=item.get('title'), + cover=item.get('imageUrl'), + geoblocked=item.get('geoBlocked'), + )) + elif item.get('target', {}).get('type') == self.CONTENT_TYPE_PROGRAM: + items.append(Program( + program_id=item.get('target', {}).get('id'), + name=item.get('title'), + cover=item.get('imageUrl'), + geoblocked=item.get('geoBlocked'), + )) return items @@ -534,18 +532,23 @@ def get_episode(self, episode_id): def do_search(self, search): """ Do a search in the full catalogue. :type search: str - :rtype list[Content] + :rtype list[Union[Movie, Program]] """ response = self._get_url('/%s/autocomplete/?maxItems=%d&keywords=%s' % (self._mode(), 50, quote(search))) results = json.loads(response) items = [] for item in results.get('suggestions', []): - items.append(Content( - content_id=item.get('id'), - name=item.get('name'), - video_type=item.get('type'), - )) + if item.get('type') == self.CONTENT_TYPE_MOVIE: + items.append(Movie( + movie_id=item.get('id'), + name=item.get('name'), + )) + elif item.get('type') == self.CONTENT_TYPE_PROGRAM: + items.append(Program( + program_id=item.get('id'), + name=item.get('name'), + )) return items @@ -554,7 +557,7 @@ def _get_url(self, url): :type url: str :rtype str """ - headers = self._headers + headers = self._HEADERS token = self._auth.get_token() if token: headers['x-dpp-jwt'] = token @@ -578,7 +581,7 @@ def _put_url(self, url): :type url: str :rtype str """ - headers = self._headers + headers = self._HEADERS token = self._auth.get_token() if token: headers['x-dpp-jwt'] = token @@ -602,7 +605,7 @@ def _delete_url(self, url): :type url: str :rtype str """ - headers = self._headers + headers = self._HEADERS token = self._auth.get_token() if token: headers['x-dpp-jwt'] = token From 0c914b2da878672492e4203f1e8037c0f298bcb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Sun, 20 Oct 2019 15:46:23 +0200 Subject: [PATCH 3/7] Allow geoblocked exception during tests --- test/test_vtmgo.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/test/test_vtmgo.py b/test/test_vtmgo.py index 57d1b755..8a725779 100644 --- a/test/test_vtmgo.py +++ b/test/test_vtmgo.py @@ -10,6 +10,7 @@ from urllib3.exceptions import InsecureRequestWarning +from lib import GeoblockedException from resources.lib.kodiwrapper import KodiWrapper from resources.lib.vtmgo import vtmgo, vtmgostream, vtmgoauth @@ -77,9 +78,12 @@ def test_get_episode(self): # print(info) def test_get_stream(self): - info = self._vtmgostream.get_stream('episodes', 'ae0fa98d-6ed5-4f4a-8581-a051ed3bb755') - self.assertTrue(info) - # print(info) + try: + info = self._vtmgostream.get_stream('episodes', 'ae0fa98d-6ed5-4f4a-8581-a051ed3bb755') + self.assertTrue(info) + # print(info) + except GeoblockedException: + pass info = self._vtmgostream.get_stream('channels', 'd8659669-b964-414c-aa9c-e31d8d15696b') self.assertTrue(info) From bb4d1134dc7d35abd8a821c5914951258ffca827 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Sun, 20 Oct 2019 15:51:38 +0200 Subject: [PATCH 4/7] Fix test --- test/test_vtmgo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_vtmgo.py b/test/test_vtmgo.py index 8a725779..4da97ba4 100644 --- a/test/test_vtmgo.py +++ b/test/test_vtmgo.py @@ -10,7 +10,7 @@ from urllib3.exceptions import InsecureRequestWarning -from lib import GeoblockedException +from resources.lib import GeoblockedException from resources.lib.kodiwrapper import KodiWrapper from resources.lib.vtmgo import vtmgo, vtmgostream, vtmgoauth From 53135083cc66c3e1e39ed220ba2efa33ae41a35f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Sun, 20 Oct 2019 22:36:11 +0200 Subject: [PATCH 5/7] Refactor _generate_titleitem. Prepare for watched status --- resources/lib/kodiwrapper.py | 5 ++ resources/lib/plugin.py | 152 ++++++++++++++++++++--------------- resources/lib/service.py | 4 +- resources/lib/vtmgo/vtmgo.py | 49 +++++++---- 4 files changed, 127 insertions(+), 83 deletions(-) diff --git a/resources/lib/kodiwrapper.py b/resources/lib/kodiwrapper.py index 46c67321..c3e3d702 100644 --- a/resources/lib/kodiwrapper.py +++ b/resources/lib/kodiwrapper.py @@ -498,6 +498,11 @@ def delete_file(self, path): self.log("Delete file '{path}'.", LOG_DEBUG, path=path) return delete(path) + def container_refresh(self): + """ Refresh the current container """ + self.log('Execute: Container.Refresh', LOG_DEBUG) + xbmc.executebuiltin('Container.Refresh') + def end_of_directory(self): """ Close a virtual directory, required to avoid a waiting Kodi """ xbmcplugin.endOfDirectory(handle=self._handle, succeeded=False, updateListing=False, cacheToDisc=False) diff --git a/resources/lib/plugin.py b/resources/lib/plugin.py index 87f9e2cc..e6bc0d0c 100644 --- a/resources/lib/plugin.py +++ b/resources/lib/plugin.py @@ -79,14 +79,14 @@ def show_index(): info_dict={ 'plot': kodi.localize(30018), }), - TitleItem(title=kodi.localize(30019), # Continue watching - path=routing.url_for(show_continuewatching if not kids else show_kids_continuewatching), - art_dict={ - 'icon': 'DefaultInProgressShows.png' - }, - info_dict={ - 'plot': kodi.localize(30020), - }), + # TitleItem(title=kodi.localize(30019), # Continue watching + # path=routing.url_for(show_continuewatching if not kids else show_kids_continuewatching), + # art_dict={ + # 'icon': 'DefaultInProgressShows.png' + # }, + # info_dict={ + # 'plot': kodi.localize(30020), + # }), ]) # Only provide YouTube option when plugin.video.youtube is available @@ -449,7 +449,8 @@ def show_mylist(): listing = [] for item in mylist: - listing.append(_generate_titleitem(item, True)) + item.my_list = True + listing.append(_generate_titleitem(item)) # Sort categories by default like in VTM GO. kodi.show_listing(listing, 30017, content='tvshows') @@ -479,6 +480,7 @@ def mylist_del(video_type, content_id): """ Remove an item from "My List" """ vtm_go.del_mylist(video_type, content_id) kodi.end_of_directory() + kodi.container_refresh() @routing.route('/kids/continuewatching') @@ -498,7 +500,14 @@ def show_continuewatching(): listing = [] for item in mylist: - listing.append(_generate_titleitem(item, True)) + titleitem = _generate_titleitem(item, progress=True) + + # Add Program Name to title since this list contains episodes from multiple programs + title = '%s - %s' % (titleitem.info_dict.get('tvshowtitle'), titleitem.info_dict.get('title')) + titleitem.title = title + titleitem.info_dict['title'] = title + + listing.append(titleitem) # Sort categories by default like in VTM GO. kodi.show_listing(listing, 30019, content='episodes', sort='label') @@ -643,36 +652,7 @@ def show_program_season(program, season): listing = [] for s in seasons: for episode in s.episodes.values(): - listing.append( - TitleItem(title=episode.name, - path=routing.url_for(play, category='episodes', item=episode.episode_id), - art_dict={ - 'banner': program_obj.cover, - 'fanart': program_obj.cover, - 'thumb': episode.cover, - }, - info_dict={ - 'tvshowtitle': program_obj.name, - 'title': episode.name, - 'tagline': program_obj.description, - 'plot': _format_plot(episode), - 'duration': episode.duration, - 'season': episode.season, - 'episode': episode.number, - 'mediatype': 'episode', - 'set': program_obj.name, - 'studio': episode.channel, - 'aired': episode.aired, - 'mpaa': ', '.join(episode.legal) if hasattr(episode, 'legal') and episode.legal else kodi.localize(30216), - }, - stream_dict={ - 'duration': episode.duration, - 'codec': 'h264', - 'height': 1080, - 'width': 1920, - }, - is_playable=True) - ) + listing.append(_generate_titleitem(episode)) # Sort by episode number by default. Takes seasons into account. kodi.show_listing(listing, 30003, content='episodes', sort='episode') @@ -753,7 +733,7 @@ def show_search(query=None): kodi.show_listing(listing, 30009, content='tvshows') -def _generate_titleitem(item, my_list=False): +def _generate_titleitem(item, progress=False): """ Generate a TitleItem based on a Movie, Program or Episode. :type item: Union[Movie, Program, Episode] :rtype TitleItem @@ -767,9 +747,13 @@ def _generate_titleitem(item, my_list=False): 'title': item.name, 'plot': item.description, } + prop_dict = {} + # + # Movie + # if isinstance(item, Movie): - if my_list: + if item.my_list: context_menu = [( kodi.localize(30051), # Remove from My List 'XBMC.Container.Update(%s)' % @@ -812,8 +796,11 @@ def _generate_titleitem(item, my_list=False): context_menu=context_menu, is_playable=True) + # + # Program + # if isinstance(item, Program): - if my_list: + if item.my_list: context_menu = [( kodi.localize(30051), # Remove from My List 'XBMC.Container.Update(%s)' % @@ -850,6 +837,9 @@ def _generate_titleitem(item, my_list=False): info_dict=info_dict, context_menu=context_menu) + # + # Episode + # if isinstance(item, Episode): context_menu = [( kodi.localize(30052), # Go to Program @@ -858,38 +848,67 @@ def _generate_titleitem(item, my_list=False): )] info_dict.update({ - 'title': '%dx%02d. %s' % (item.season, item.number, item.name), - 'mediatype': 'episode', + 'tvshowtitle': item.program_name, + 'title': item.name, + 'plot': _format_plot(item), + 'duration': item.duration, 'season': item.season, 'episode': item.number, + 'mediatype': 'episode', + 'set': item.program_name, + 'studio': item.channel, + 'aired': item.aired, + 'mpaa': ', '.join(item.legal) if hasattr(item, 'legal') and item.legal else kodi.localize(30216), }) + + if progress and item.watched: + info_dict.update({ + 'playcount': 1, + }) + stream_dict = { 'codec': 'h264', + 'duration': item.duration, 'height': 1080, 'width': 1920, } - # Get program details + # Get program and episode details from cache program = vtm_go.get_program(item.program_id, only_cache=True) - episode = vtm_go.get_episode_from_program(item.program_id, item.episode_id, only_cache=True) - if program and episode: - art_dict.update({ - 'fanart': episode.cover, - 'banner': episode.cover, - }) - info_dict.update({ - 'title': '%dx%02d. %s - %s' % (item.season, item.number, program.name, episode.name), - 'tvshowtitle': program.name, - 'tagline': program.description, - 'plot': _format_plot(episode), - 'duration': episode.duration, - 'set': program.name, - 'studio': episode.channel, - 'aired': episode.aired, - 'mpaa': ', '.join(episode.legal) if hasattr(episode, 'legal') and episode.legal else kodi.localize(30216), - }) - stream_dict.update({ - 'duration': episode.duration, + if program: + episode = vtm_go.get_episode_from_program(program, item.episode_id) + if episode: + art_dict.update({ + 'fanart': episode.cover, + 'banner': episode.cover, + }) + info_dict.update({ + 'tvshowtitle': program.name, + 'title': episode.name, + 'plot': _format_plot(episode), + 'duration': episode.duration, + 'season': episode.season, + 'episode': episode.number, + 'set': program.name, + 'studio': episode.channel, + 'aired': episode.aired, + 'mpaa': ', '.join(episode.legal) if hasattr(episode, 'legal') and episode.legal else kodi.localize(30216), + }) + + if progress and item.watched: + info_dict.update({ + 'playcount': 1, + }) + + stream_dict.update({ + 'duration': episode.duration, + }) + + # Add progress info + if progress and not item.watched and item.progress: + prop_dict.update({ + 'ResumeTime': item.progress, + 'TotalTime': item.progress + 1, }) return TitleItem(title=info_dict['title'], @@ -897,6 +916,7 @@ def _generate_titleitem(item, my_list=False): art_dict=art_dict, info_dict=info_dict, stream_dict=stream_dict, + prop_dict=prop_dict, context_menu=context_menu, is_playable=True) @@ -944,8 +964,8 @@ def play(category, item): kodi.show_ok_dialog(message=kodi.localize(30708)) # Please reboot Kodi return - # Get stream information try: + # Get stream information resolved_stream = _vtmgostream.get_stream(category, item) except GeoblockedException: diff --git a/resources/lib/service.py b/resources/lib/service.py index 388827e1..5d3da95f 100644 --- a/resources/lib/service.py +++ b/resources/lib/service.py @@ -5,7 +5,7 @@ from time import time -from resources.lib.kodiwrapper import KodiWrapper, LOG_INFO +from resources.lib.kodiwrapper import KodiWrapper, LOG_INFO, LOG_DEBUG from resources.lib.vtmgo.vtmgo import VtmGo, Program, Movie from xbmc import Monitor @@ -38,7 +38,7 @@ def run(self): def onSettingsChanged(self): """ Callback when a setting has changed """ - self.kodi.log('IN VTM GO: Settings changed') + self.kodi.log('IN VTM GO: Settings changed', LOG_DEBUG) # Refresh our VtmGo instance self.vtm_go = VtmGo(self.kodi) diff --git a/resources/lib/vtmgo/vtmgo.py b/resources/lib/vtmgo/vtmgo.py index 64d606fe..f4a9fc46 100644 --- a/resources/lib/vtmgo/vtmgo.py +++ b/resources/lib/vtmgo/vtmgo.py @@ -77,7 +77,7 @@ class Movie: """ Defines a Movie """ def __init__(self, movie_id=None, name=None, description=None, year=None, cover=None, duration=None, remaining=None, geoblocked=None, - channel=None, legal=None, aired=None): + channel=None, legal=None, aired=None, my_list=None): """ :type movie_id: str :type name: str @@ -90,6 +90,7 @@ def __init__(self, movie_id=None, name=None, description=None, year=None, cover= :type channel: str :type legal: str :type aired: str + :type my_list: bool """ self.movie_id = movie_id self.name = name @@ -102,6 +103,7 @@ def __init__(self, movie_id=None, name=None, description=None, year=None, cover= self.channel = channel self.legal = legal self.aired = aired + self.my_list = my_list def __repr__(self): return "%r" % self.__dict__ @@ -110,7 +112,7 @@ def __repr__(self): class Program: """ Defines a Program """ - def __init__(self, program_id=None, name=None, description=None, cover=None, seasons=None, geoblocked=None, channel=None, legal=None): + def __init__(self, program_id=None, name=None, description=None, cover=None, seasons=None, geoblocked=None, channel=None, legal=None, my_list=None): """ :type program_id: str :type name: str @@ -120,6 +122,7 @@ def __init__(self, program_id=None, name=None, description=None, cover=None, sea :type geoblocked: bool :type channel: str :type legal: str + :type my_list: bool """ self.program_id = program_id self.name = name @@ -129,6 +132,7 @@ def __init__(self, program_id=None, name=None, description=None, cover=None, sea self.geoblocked = geoblocked self.channel = channel self.legal = legal + self.my_list = my_list def __repr__(self): return "%r" % self.__dict__ @@ -160,12 +164,12 @@ def __repr__(self): class Episode: """ Defines an Episode """ - def __init__(self, episode_id=None, program_id=None, number=None, season=None, name=None, description=None, cover=None, duration=None, remaining=None, - geoblocked=None, - channel=None, legal=None, aired=None): + def __init__(self, episode_id=None, program_id=None, program_name=None, number=None, season=None, name=None, description=None, cover=None, duration=None, + remaining=None, geoblocked=None, channel=None, legal=None, aired=None, progress=None, watched=False): """ :type episode_id: str :type program_id: str + :type program_name: str :type number: int :type season: str :type name: str @@ -177,10 +181,13 @@ def __init__(self, episode_id=None, program_id=None, number=None, season=None, n :type channel: str :type legal: str :type aired: str + :type progress: int + :type watched: bool """ import re self.episode_id = episode_id self.program_id = program_id + self.program_name = program_name self.number = int(number) self.season = int(season) self.name = re.compile('^%d. ' % number).sub('', name) # Strip episode from name @@ -192,6 +199,8 @@ def __init__(self, episode_id=None, program_id=None, number=None, season=None, n self.channel = channel self.legal = legal self.aired = aired + self.progress = progress + self.watched = watched def __repr__(self): return "%r" % self.__dict__ @@ -294,14 +303,22 @@ def get_swimlane(self, swimlane=None): )) elif item.get('target', {}).get('type') == self.CONTENT_TYPE_EPISODE: + if swimlane == 'continue-watching': + title = '%dx%02d' % (item.get('target', {}).get('seasonIndex'), item.get('target', {}).get('episodeIndex')) + else: + title = item.get('title') + items.append(Episode( episode_id=item.get('target', {}).get('id'), program_id=item.get('target', {}).get('programId'), + program_name=item.get('target', {}).get('programName'), number=item.get('target', {}).get('episodeIndex'), season=item.get('target', {}).get('seasonIndex'), - name='%dx%02d. ' % (item.get('target', {}).get('episodeIndex'), item.get('target', {}).get('seasonIndex')) + item.get('title'), + name=title, geoblocked=item.get('geoBlocked'), cover=item.get('imageUrl'), + progress=item.get('playerPositionSeconds'), + watched=False, )) return items @@ -458,6 +475,7 @@ def get_program(self, program_id, only_cache=False): episodes[item_episode.get('index')] = Episode( episode_id=item_episode.get('id'), program_id=program_id, + program_name=program.get('name'), number=item_episode.get('index'), season=item_season.get('index'), name=item_episode.get('name'), @@ -469,6 +487,8 @@ def get_program(self, program_id, only_cache=False): channel=channel, legal=program.get('legalIcons'), aired=item_episode.get('broadcastTimestamp'), + progress=item_episode.get('playerPositionSeconds', 0), + watched=item_episode.get('doneWatching', False), ) seasons[item_season.get('index')] = Season( @@ -491,23 +511,19 @@ def get_program(self, program_id, only_cache=False): legal=program.get('legalIcons'), ) - def get_episode_from_program(self, program_id, episode_id, only_cache=False): - """ Get the details of the specified episode. - :type program_id: str + @staticmethod + def get_episode_from_program(program, episode_id): + """ Extract the specified episode from the program data. + :type program: Program :type episode_id: str - :type only_cache: bool :rtype Episode """ - program = self.get_program(program_id, only_cache=only_cache) - if not program: - raise UnavailableException() - for season in program.seasons.values(): for episode in season.episodes.values(): if episode.episode_id == episode_id: return episode - raise UnavailableException() + return None def get_episode(self, episode_id): """ Get the details of the specified episode. @@ -522,11 +538,14 @@ def get_episode(self, episode_id): return Episode( episode_id=episode.get('id'), + program_id=episode.get('programId'), + program_name=episode.get('programName'), number=episode.get('index'), season=episode.get('seasonIndex'), name=episode.get('name'), description=episode.get('description'), cover=episode.get('bigPhotoUrl'), + progress=episode.get('playerPositionSeconds'), ) def do_search(self, search): From 7e5414a0c6e34be71e23ce3c156880fb24ada896 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Sun, 20 Oct 2019 22:43:45 +0200 Subject: [PATCH 6/7] Ignore too-many-branches for now --- .pylintrc | 1 + 1 file changed, 1 insertion(+) diff --git a/.pylintrc b/.pylintrc index 84a048af..e29df18b 100644 --- a/.pylintrc +++ b/.pylintrc @@ -9,6 +9,7 @@ disable= old-style-class, too-few-public-methods, too-many-arguments, + too-many-branches, too-many-instance-attributes, too-many-locals, too-many-public-methods, From db256b39144eb40bb1075587e67be999cf172ac1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Sun, 20 Oct 2019 23:17:15 +0200 Subject: [PATCH 7/7] Geoblocking fix --- test/test_vtmgo.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/test/test_vtmgo.py b/test/test_vtmgo.py index 4da97ba4..a894d1c0 100644 --- a/test/test_vtmgo.py +++ b/test/test_vtmgo.py @@ -85,15 +85,21 @@ def test_get_stream(self): except GeoblockedException: pass - info = self._vtmgostream.get_stream('channels', 'd8659669-b964-414c-aa9c-e31d8d15696b') - self.assertTrue(info) - # print(info) + try: + info = self._vtmgostream.get_stream('channels', 'd8659669-b964-414c-aa9c-e31d8d15696b') + self.assertTrue(info) + # print(info) + except GeoblockedException: + pass def test_get_stream_with_subtitles(self): - # 13 Geboden - Episode 2 - info = self._vtmgostream.get_stream('episodes', '2fafb247-0368-46d4-bdcf-fb209420e715') - self.assertTrue(info) - # print(info) + try: + # 13 Geboden - Episode 2 + info = self._vtmgostream.get_stream('episodes', '2fafb247-0368-46d4-bdcf-fb209420e715') + self.assertTrue(info) + # print(info) + except GeoblockedException: + pass if __name__ == '__main__':