def __init__(self): """PlayerInfo initialisation""" self.resumepoints = ResumePoints() self.apihelper = ApiHelper(Favorites(), self.resumepoints) self.last_pos = None self.listen = False self.paused = False self.total = 100 self.positionthread = None self.quit = Event() self.asset_str = None # FIXME On Kodi 17, use ListItem.Filenameandpath because Player.FilenameAndPath returns the stream manifest url and # this definitely breaks "Up Next" on Kodi 17, but this is not supported or available through the Kodi add-on repo anyway self.path_infolabel = 'ListItem.Filenameandpath' if kodi_version_major( ) < 18 else 'Player.FilenameAndPath' self.path = None self.title = None self.ep_id = None self.episode_id = None self.episode_title = None self.video_id = None from random import randint self.thread_id = randint(1, 10001) log(3, '[PlayerInfo {id}] Initialized', id=self.thread_id) super(PlayerInfo, self).__init__()
def init_watching_activity(self): ''' Only load components for watching activity when needed ''' if self._resumepoints.is_activated(): if not self._playerinfo: self._playerinfo = PlayerInfo(info=self.handle_info) if not self._favorites: self._favorites = Favorites() if not self._apihelper: self._apihelper = ApiHelper(self._favorites, self._resumepoints)
class TestResumePoints(unittest.TestCase): _favorites = Favorites() _resumepoints = ResumePoints() _apihelper = ApiHelper(_favorites, _resumepoints) @unittest.skipUnless(addon.settings.get('username'), 'Skipping as VRT username is missing.') @unittest.skipUnless(addon.settings.get('password'), 'Skipping as VRT password is missing.') def test_get_watchlater_episodes(self): ''' Test items, sort and order ''' episode_items, sort, ascending, content = self._apihelper.list_episodes( page=1, variety='watchlater') self.assertTrue(episode_items) self.assertEqual(sort, 'dateadded') self.assertFalse(ascending) self.assertEqual(content, 'episodes') @unittest.skipUnless(addon.settings.get('username'), 'Skipping as VRT username is missing.') @unittest.skipUnless(addon.settings.get('password'), 'Skipping as VRT password is missing.') def test_get_continue_episodes(self): ''' Test items, sort and order ''' episode_items, sort, ascending, content = self._apihelper.list_episodes( page=1, variety='continue') self.assertTrue(episode_items) self.assertEqual(sort, 'dateadded') self.assertFalse(ascending) self.assertEqual(content, 'episodes') @unittest.skipUnless(addon.settings.get('username'), 'Skipping as VRT username is missing.') @unittest.skipUnless(addon.settings.get('password'), 'Skipping as VRT password is missing.') def test_update_watchlist(self): self._resumepoints.refresh(ttl=0) assetuuid, first_entry = next( iter(self._resumepoints._resumepoints.items())) # pylint: disable=protected-access print('%s = %s' % (assetuuid, first_entry)) url = first_entry.get('value').get('url') self._resumepoints.watchlater(uuid=assetuuid, title='Foo bar', url=url) self._resumepoints.unwatchlater(uuid=assetuuid, title='Foo bar', url=url) self._resumepoints.refresh(ttl=0) assetuuid, first_entry = next( iter(self._resumepoints._resumepoints.items())) # pylint: disable=protected-access print('%s = %s' % (assetuuid, first_entry)) def test_assetpath_to_uuid(self): self.assertEqual(None, self._resumepoints.assetpath_to_uuid(None)) assetpath = '/content/dam/vrt/2019/08/14/woodstock-depot_WP00157456' uuid = 'contentdamvrt20190814woodstockdepotwp00157456' self.assertEqual(uuid, self._resumepoints.assetpath_to_uuid(assetpath))
def search(self, keywords=None, page=None): ''' The VRT NU add-on Search functionality and results ''' if keywords is None: keywords = get_search_string() if not keywords: end_of_directory() return from statichelper import realpage page = realpage(page) self.add(keywords) from apihelper import ApiHelper search_items, sort, ascending, content = ApiHelper( self._favorites, self._resumepoints).list_search(keywords, page=page) if not search_items: ok_dialog(heading=localize(30135), message=localize(30136, keywords=keywords)) end_of_directory() return # Add 'More...' entry at the end from helperobjects import TitleItem if len(search_items) == 50: search_items.append( TitleItem( title=localize(30300), path=url_for('search_query', keywords=keywords, page=page + 1), art_dict=dict(thumb='DefaultAddonSearch.png'), info_dict=dict(), )) self._favorites.refresh(ttl=60 * 60) show_listing(search_items, category=30032, sort=sort, ascending=ascending, content=content, cache=False)
class TestSearch(unittest.TestCase): _favorites = Favorites() _resumepoints = ResumePoints() _apihelper = ApiHelper(_favorites, _resumepoints) def test_search_journaal(self): ''' Test for journaal ''' search_items, sort, ascending, content = self._apihelper.list_search( 'journaal', page=1) # Test we get a non-empty search result self.assertEqual(len(search_items), 50) self.assertEqual(sort, 'dateadded') self.assertFalse(ascending) self.assertEqual(content, 'episodes') def test_search_journaal_page2(self): ''' Test for journaal ''' search_items, sort, ascending, content = self._apihelper.list_search( 'journaal', page=2) # Test we get a non-empty search result self.assertEqual(len(search_items), 50) self.assertEqual(sort, 'dateadded') self.assertFalse(ascending) self.assertEqual(content, 'episodes') def test_search_weer(self): ''' Test for journaal ''' search_items, sort, ascending, content = self._apihelper.list_search( 'weer', page=1) # Test we get a non-empty search result self.assertEqual(len(search_items), 50) self.assertEqual(sort, 'dateadded') self.assertFalse(ascending) self.assertEqual(content, 'episodes') def test_search_unicode(self): ''' Test for unicode ''' search_items, sort, ascending, content = self._apihelper.list_search( 'René', page=1) # Test we get a non-empty search result self.assertGreater(len(search_items), 0) self.assertEqual(sort, 'dateadded') self.assertFalse(ascending) self.assertEqual(content, 'episodes') @staticmethod def test_search_empty(): ''' Test for empty search ''' Search().search(keywords='')
def init_watching_activity(self): """Only load components for watching activity when needed""" if self._resumepoints.is_activated(): if not self._playerinfo: self._playerinfo = PlayerInfo() if not self._favorites: self._favorites = Favorites() if not self._apihelper: self._apihelper = ApiHelper(self._favorites, self._resumepoints) else: self._playerinfo = None
def search(self, keywords=None, page=0, edit=False): """The VRT NU add-on Search functionality and results""" if keywords is None or edit is True: keywords = get_search_string(keywords) if not keywords: end_of_directory() return if edit is True: container_update(url_for('search_query', keywords=keywords)) return from apihelper import ApiHelper from utils import realpage page = realpage(page) self.add(keywords) search_items, sort, ascending, content = ApiHelper(self._favorites, self._resumepoints).list_search(keywords, page=page) if not search_items: ok_dialog(heading=localize(30135), message=localize(30136, keywords=keywords)) end_of_directory() return # Add 'More…' entry at the end from helperobjects import TitleItem if len(search_items) == get_setting_int('itemsperpage', default=50): search_items.append(TitleItem( label=colour(localize(30300)), # More… path=url_for('search_query', keywords=keywords, page=page + 1), art_dict=dict(thumb='DefaultAddonSearch.png'), info_dict={}, )) self._favorites.refresh(ttl=ttl('indirect')) show_listing(search_items, category=30032, sort=sort, ascending=ascending, content=content, cache=False)
def __init__(self): """Initialise object""" self._favorites = Favorites() self._resumepoints = ResumePoints() self._apihelper = ApiHelper(self._favorites, self._resumepoints) wait_for_resumepoints()
class VRTPlayer: """An object providing all methods for Kodi menu generation""" def __init__(self): """Initialise object""" self._favorites = Favorites() self._resumepoints = ResumePoints() self._apihelper = ApiHelper(self._favorites, self._resumepoints) wait_for_resumepoints() def show_main_menu(self): """The VRT NU add-on main menu""" # self._favorites.refresh(ttl=ttl('indirect')) main_items = [] # Only add 'My favorites' when it has been activated if self._favorites.is_activated(): main_items.append(TitleItem( label=localize(30010), # My favorites path=url_for('favorites_menu'), art_dict=dict(thumb='DefaultFavourites.png'), info_dict=dict(plot=localize(30011)), )) main_items.extend([ TitleItem(label=localize(30012), # All programs path=url_for('programs'), art_dict=dict(thumb='DefaultMovieTitle.png'), info_dict=dict(plot=localize(30013))), TitleItem(label=localize(30014), # Categories path=url_for('categories'), art_dict=dict(thumb='DefaultGenre.png'), info_dict=dict(plot=localize(30015))), TitleItem(label=localize(30016), # Channels path=url_for('channels'), art_dict=dict(thumb='DefaultTags.png'), info_dict=dict(plot=localize(30017))), TitleItem(label=localize(30018), # Live TV path=url_for('livetv'), art_dict=dict(thumb='DefaultTVShows.png'), info_dict=dict(plot=localize(30019))), TitleItem(label=localize(30020), # Recent items path=url_for('recent'), art_dict=dict(thumb='DefaultRecentlyAddedEpisodes.png'), info_dict=dict(plot=localize(30021))), TitleItem(label=localize(30022), # Soon offline path=url_for('offline'), art_dict=dict(thumb='DefaultYear.png'), info_dict=dict(plot=localize(30023))), TitleItem(label=localize(30024), # Featured content path=url_for('featured'), art_dict=dict(thumb='DefaultCountry.png'), info_dict=dict(plot=localize(30025))), TitleItem(label=localize(30026), # TV guide path=url_for('tvguide'), art_dict=dict(thumb='DefaultAddonTvInfo.png'), info_dict=dict(plot=localize(30027))), TitleItem(label=localize(30028), # Search path=url_for('search'), art_dict=dict(thumb='DefaultAddonsSearch.png'), info_dict=dict(plot=localize(30029))), ]) show_listing(main_items, cache=False) # No category self._version_check() def _version_check(self): first_run, settings_version, addon_version = self._first_run() if first_run: # 2.0.0 version: changed plugin:// url interface: show warning that Kodi favourites and what-was-watched will break if settings_version == '' and has_credentials(): ok_dialog(localize(30978), localize(30979)) if addon_version == '2.2.1': # 2.2.1 version: changed artwork: delete old cached artwork delete_cached_thumbnail(get_addon_info('fanart').replace('.png', '.jpg')) delete_cached_thumbnail(get_addon_info('icon')) # 2.2.1 version: moved tokens: delete old tokens from tokenresolver import TokenResolver TokenResolver().delete_tokens() # Make user aware that timeshift functionality will not work without ISA when user starts up the first time if settings_version == '' and kodi_version_major() > 17 and not has_inputstream_adaptive(): ok_dialog(message=localize(30988)) @staticmethod def _first_run(): '''Check if this add-on version is run for the first time''' # Get version from settings.xml settings_version = get_setting('version', default='') # Get version from addon.xml addon_version = get_addon_info('version') # Compare versions (settings_version was not present in version 1.10.0 and older) settings_comp = tuple(map(int, settings_version.split('+')[0].split('.'))) if settings_version != '' else (1, 10, 0) addon_comp = tuple(map(int, addon_version.split('+')[0].split('.'))) if addon_comp > settings_comp: # New version found, save addon version to settings set_setting('version', addon_version) return True, settings_version, addon_version return False, settings_version, addon_version def show_favorites_menu(self): """The VRT NU addon 'My programs' menu""" self._favorites.refresh(ttl=ttl('indirect')) favorites_items = [ TitleItem(label=localize(30040), # My programs path=url_for('favorites_programs'), art_dict=dict(thumb='DefaultMovieTitle.png'), info_dict=dict(plot=localize(30041))), TitleItem(label=localize(30048), # My recent items path=url_for('favorites_recent'), art_dict=dict(thumb='DefaultRecentlyAddedEpisodes.png'), info_dict=dict(plot=localize(30049))), TitleItem(label=localize(30050), # My soon offline path=url_for('favorites_offline'), art_dict=dict(thumb='DefaultYear.png'), info_dict=dict(plot=localize(30051))), ] # Only add 'My watch later' and 'Continue watching' when it has been activated if self._resumepoints.is_activated(): favorites_items.append(TitleItem( label=localize(30052), # My watch later path=url_for('resumepoints_watchlater'), art_dict=dict(thumb='DefaultVideoPlaylists.png'), info_dict=dict(plot=localize(30053)), )) favorites_items.append(TitleItem( label=localize(30054), # Continue Watching path=url_for('resumepoints_continue'), art_dict=dict(thumb='DefaultInProgressShows.png'), info_dict=dict(plot=localize(30055)), )) if get_setting_bool('addmymovies', default=True): favorites_items.append( TitleItem(label=localize(30042), # My movies path=url_for('categories', category='films'), art_dict=dict(thumb='DefaultAddonVideo.png'), info_dict=dict(plot=localize(30043))), ) if get_setting_bool('addmydocu', default=True): favorites_items.append( TitleItem(label=localize(30044), # My documentaries path=url_for('favorites_docu'), art_dict=dict(thumb='DefaultMovies.png'), info_dict=dict(plot=localize(30045))), ) if get_setting_bool('addmymusic', default=True): favorites_items.append( TitleItem(label=localize(30046), # My music path=url_for('favorites_music'), art_dict=dict(thumb='DefaultAddonMusic.png'), info_dict=dict(plot=localize(30047))), ) show_listing(favorites_items, category=30010, cache=False) # My favorites # Show dialog when no favorites were found if not self._favorites.titles(): ok_dialog(heading=localize(30415), message=localize(30416)) def show_favorites_docu_menu(self): """The VRT NU add-on 'My documentaries' listing menu""" self._favorites.refresh(ttl=ttl('indirect')) self._resumepoints.refresh(ttl=ttl('indirect')) episode_items, sort, ascending, content = self._apihelper.list_episodes(category='docu', season='allseasons', programtype='oneoff') show_listing(episode_items, category=30044, sort=sort, ascending=ascending, content=content, cache=False) def show_favorites_music_menu(self): """The VRT NU add-on 'My music' listing menu""" self._favorites.refresh(ttl=ttl('indirect')) self._resumepoints.refresh(ttl=ttl('indirect')) episode_items, sort, ascending, content = self._apihelper.list_episodes(category='muziek', season='allseasons', programtype='oneoff') show_listing(episode_items, category=30046, sort=sort, ascending=ascending, content=content, cache=False) def show_tvshow_menu(self, use_favorites=False): """The VRT NU add-on 'All programs' listing menu""" # My favorites menus may need more up-to-date favorites self._favorites.refresh(ttl=ttl('direct' if use_favorites else 'indirect')) self._resumepoints.refresh(ttl=ttl('direct' if use_favorites else 'indirect')) tvshow_items = self._apihelper.list_tvshows(use_favorites=use_favorites) show_listing(tvshow_items, category=30440, sort='label', content='tvshows') # A-Z def show_category_menu(self, category=None): """The VRT NU add-on 'Categories' listing menu""" if category: self._favorites.refresh(ttl=ttl('indirect')) self._resumepoints.refresh(ttl=ttl('indirect')) tvshow_items = self._apihelper.list_tvshows(category=category) from data import CATEGORIES category_msgctxt = find_entry(CATEGORIES, 'id', category).get('msgctxt') show_listing(tvshow_items, category=category_msgctxt, sort='label', content='tvshows') else: category_items = self._apihelper.list_categories() show_listing(category_items, category=30014, sort='unsorted', content='files') # Categories def show_channels_menu(self, channel=None): """The VRT NU add-on 'Channels' listing menu""" if channel: from tvguide import TVGuide self._favorites.refresh(ttl=ttl('indirect')) self._resumepoints.refresh(ttl=ttl('indirect')) channel_items = self._apihelper.list_channels(channels=[channel]) # Live TV channel_items.extend(TVGuide().get_channel_items(channel=channel)) # TV guide channel_items.extend(self._apihelper.list_youtube(channels=[channel])) # YouTube channel_items.extend(self._apihelper.list_tvshows(channel=channel)) # TV shows from data import CHANNELS channel_name = find_entry(CHANNELS, 'name', channel).get('label') show_listing(channel_items, category=channel_name, sort='unsorted', content='tvshows', cache=False) # Channel else: channel_items = self._apihelper.list_channels(live=False) show_listing(channel_items, category=30016, cache=False) def show_featured_menu(self, feature=None): """The VRT NU add-on 'Featured content' listing menu""" if feature: self._favorites.refresh(ttl=ttl('indirect')) self._resumepoints.refresh(ttl=ttl('indirect')) tvshow_items = self._apihelper.list_tvshows(feature=feature) from data import FEATURED feature_msgctxt = find_entry(FEATURED, 'id', feature).get('msgctxt') show_listing(tvshow_items, category=feature_msgctxt, sort='label', content='tvshows', cache=False) else: featured_items = self._apihelper.list_featured() show_listing(featured_items, category=30024, sort='label', content='files') def show_livetv_menu(self): """The VRT NU add-on 'Live TV' listing menu""" channel_items = self._apihelper.list_channels() show_listing(channel_items, category=30018, cache=False) def show_episodes_menu(self, program, season=None): """The VRT NU add-on episodes listing menu""" self._favorites.refresh(ttl=ttl('indirect')) self._resumepoints.refresh(ttl=ttl('indirect')) episode_items, sort, ascending, content = self._apihelper.list_episodes(program=program, season=season) # FIXME: Translate program in Program Title show_listing(episode_items, category=program.title(), sort=sort, ascending=ascending, content=content, cache=False) def show_recent_menu(self, page=0, use_favorites=False): """The VRT NU add-on 'Most recent' and 'My most recent' listing menu""" # My favorites menus may need more up-to-date favorites self._favorites.refresh(ttl=ttl('direct' if use_favorites else 'indirect')) self._resumepoints.refresh(ttl=ttl('direct' if use_favorites else 'indirect')) page = realpage(page) episode_items, sort, ascending, content = self._apihelper.list_episodes(page=page, use_favorites=use_favorites, variety='recent') # Add 'More...' entry at the end if len(episode_items) == get_setting_int('itemsperpage', default=50): recent = 'favorites_recent' if use_favorites else 'recent' episode_items.append(TitleItem( label=colour(localize(30300)), path=url_for(recent, page=page + 1), art_dict=dict(thumb='DefaultRecentlyAddedEpisodes.png'), info_dict=dict(), )) show_listing(episode_items, category=30020, sort=sort, ascending=ascending, content=content, cache=False) def show_offline_menu(self, page=0, use_favorites=False): """The VRT NU add-on 'Soon offline' and 'My soon offline' listing menu""" # My favorites menus may need more up-to-date favorites self._favorites.refresh(ttl=ttl('direct' if use_favorites else 'indirect')) self._resumepoints.refresh(ttl=ttl('direct' if use_favorites else 'indirect')) page = realpage(page) items_per_page = get_setting_int('itemsperpage', default=50) sort_key = 'assetOffTime' episode_items, sort, ascending, content = self._apihelper.list_episodes(page=page, items_per_page=items_per_page, use_favorites=use_favorites, variety='offline', sort_key=sort_key) # Add 'More...' entry at the end if len(episode_items) == items_per_page: offline = 'favorites_offline' if use_favorites else 'offline' episode_items.append(TitleItem( label=localize(30300), path=url_for(offline, page=page + 1), art_dict=dict(thumb='DefaultYear.png'), info_dict=dict(), )) show_listing(episode_items, category=30022, sort=sort, ascending=ascending, content=content, cache=False) def show_watchlater_menu(self, page=0): """The VRT NU add-on 'My watch later' listing menu""" # My watch later menu may need more up-to-date favorites self._favorites.refresh(ttl=ttl('direct')) self._resumepoints.refresh(ttl=ttl('direct')) page = realpage(page) episode_items, sort, ascending, content = self._apihelper.list_episodes(page=page, variety='watchlater') show_listing(episode_items, category=30052, sort=sort, ascending=ascending, content=content, cache=False) def show_continue_menu(self, page=0): """The VRT NU add-on 'Continue waching' listing menu""" # Continue watching menu may need more up-to-date favorites self._favorites.refresh(ttl=ttl('direct')) self._resumepoints.refresh(ttl=ttl('direct')) page = realpage(page) episode_items, sort, ascending, content = self._apihelper.list_episodes(page=page, variety='continue') show_listing(episode_items, category=30054, sort=sort, ascending=ascending, content=content, cache=False) def play_latest_episode(self, program): """A hidden feature in the VRT NU add-on to play the latest episode of a program""" video = self._apihelper.get_latest_episode(program) if not video: log_error('Play latest episode failed, program {program}', program=program) ok_dialog(message=localize(30954)) end_of_directory() return self.play(video) def play_episode_by_air_date(self, channel, start_date, end_date): """Play an episode of a program given the channel and the air date in iso format (2019-07-06T19:35:00)""" video = self._apihelper.get_episode_by_air_date(channel, start_date, end_date) if video and video.get('errorlabel'): ok_dialog(message=localize(30986, title=video.get('errorlabel'))) end_of_directory() return if not video: log_error('Play episode by air date failed, channel {channel}, start_date {start}', channel=channel, start=start_date) ok_dialog(message=localize(30954)) end_of_directory() return self.play(video) def play_episode_by_whatson_id(self, whatson_id): """Play an episode of a program given the whatson_id""" video = self._apihelper.get_single_episode(whatson_id=whatson_id) if not video: log_error('Play episode by whatson_id failed, whatson_id {whatson_id}', whatson_id=whatson_id) ok_dialog(message=localize(30954)) end_of_directory() return self.play(video) def play_upnext(self, video_id): """Play the next episode of a program by video_id""" video = self._apihelper.get_single_episode(video_id=video_id) if not video: log_error('Play Up Next with video_id {video_id} failed', video_id=video_id) ok_dialog(message=localize(30954)) end_of_directory() return self.play(video) @staticmethod def play(video): """A wrapper for playing video items""" from tokenresolver import TokenResolver from streamservice import StreamService _tokenresolver = TokenResolver() _streamservice = StreamService(_tokenresolver) stream = _streamservice.get_stream(video) if stream is None: end_of_directory() return play(stream, video.get('listitem'))
def __init__(self): ''' Initialise object ''' self._favorites = Favorites() self._resumepoints = ResumePoints() self._apihelper = ApiHelper(self._favorites, self._resumepoints)
class PlayerInfo(Player, object): # pylint: disable=useless-object-inheritance """Class for communication with Kodi player""" def __init__(self): """PlayerInfo initialisation""" self.resumepoints = ResumePoints() self.apihelper = ApiHelper(Favorites(), self.resumepoints) self.last_pos = None self.listen = False self.paused = False self.total = 100 self.positionthread = None self.quit = Event() self.asset_str = None # FIXME On Kodi 17, use ListItem.Filenameandpath because Player.FilenameAndPath returns the stream manifest url and # this definitely breaks "Up Next" on Kodi 17, but this is not supported or available through the Kodi add-on repo anyway self.path_infolabel = 'ListItem.Filenameandpath' if kodi_version_major( ) < 18 else 'Player.FilenameAndPath' self.path = None self.title = None self.ep_id = None self.episode_id = None self.episode_title = None self.video_id = None from random import randint self.thread_id = randint(1, 10001) log(3, '[PlayerInfo {id}] Initialized', id=self.thread_id) super(PlayerInfo, self).__init__() def onPlayBackStarted(self): # pylint: disable=invalid-name """Called when user starts playing a file""" self.path = getInfoLabel(self.path_infolabel) if self.path.startswith('plugin://plugin.video.vrt.nu/'): self.listen = True else: self.listen = False return log(3, '[PlayerInfo {id}] Event onPlayBackStarted', id=self.thread_id) # Set property to let wait_for_resumepoints function know that update resume is busy set_property('vrtnu_resumepoints', 'busy') # Update previous episode when using "Up Next" if self.path.startswith('plugin://plugin.video.vrt.nu/play/upnext'): self.push_position(position=self.last_pos, total=self.total) # Reset episode data self.video_id = None self.episode_id = None self.episode_title = None self.asset_str = None self.title = None ep_id = play_url_to_id(self.path) # Avoid setting resumepoints for livestreams for channel in CHANNELS: if ep_id.get('video_id') and ep_id.get('video_id') == channel.get( 'live_stream_id'): log(3, '[PlayerInfo {id}] Avoid setting resumepoints for livestream {video_id}', id=self.thread_id, video_id=ep_id.get('video_id')) self.listen = False # Reset vrtnu_resumepoints property before return set_property('vrtnu_resumepoints', None) return # Get episode data needed to update resumepoints from VRT NU Search API episode = self.apihelper.get_single_episode_data( video_id=ep_id.get('video_id'), whatson_id=ep_id.get('whatson_id'), video_url=ep_id.get('video_url')) # Avoid setting resumepoints without episode data if episode is None: # Reset vrtnu_resumepoints property before return set_property('vrtnu_resumepoints', None) return from metadata import Metadata self.video_id = episode.get('videoId') or None self.episode_id = episode.get('episodeId') or None self.episode_title = episode.get('title') or None self.asset_str = Metadata(None, None).get_asset_str(episode) self.title = episode.get('program') # Kodi 17 doesn't have onAVStarted if kodi_version_major() < 18: self.onAVStarted() def onAVStarted(self): # pylint: disable=invalid-name """Called when Kodi has a video or audiostream""" if not self.listen: return log(3, '[PlayerInfo {id}] Event onAVStarted', id=self.thread_id) self.virtualsubclip_seektozero() self.quit.clear() self.update_position() self.update_total() self.push_upnext() # StreamPosition thread keeps running when watching multiple episode with "Up Next" # only start StreamPosition thread when it doesn't exist yet. if not self.positionthread: self.positionthread = Thread(target=self.stream_position, name='StreamPosition') self.positionthread.start() def onAVChange(self): # pylint: disable=invalid-name """Called when Kodi has a video, audio or subtitle stream. Also happens when the stream changes.""" def onPlayBackSeek(self, time, seekOffset): # pylint: disable=invalid-name """Called when user seeks to a time""" if not self.listen: return log(3, '[PlayerInfo {id}] Event onPlayBackSeek time={time} offset={offset}', id=self.thread_id, time=time, offset=seekOffset) self.last_pos = time // 1000 # If we seek beyond the end, exit Player if self.last_pos >= self.total: self.quit.set() self.stop() def onPlayBackPaused(self): # pylint: disable=invalid-name """Called when user pauses a playing file""" if not self.listen: return log(3, '[PlayerInfo {id}] Event onPlayBackPaused', id=self.thread_id) self.update_position() self.push_position(position=self.last_pos, total=self.total) self.paused = True def onPlayBackResumed(self): # pylint: disable=invalid-name """Called when user resumes a paused file or a next playlist item is started""" if not self.listen: return suffix = 'after pausing' if self.paused else 'after playlist change' log(3, '[PlayerInfo {id}] Event onPlayBackResumed {suffix}', id=self.thread_id, suffix=suffix) self.paused = False def onPlayBackEnded(self): # pylint: disable=invalid-name """Called when Kodi has ended playing a file""" if not self.listen: return self.last_pos = self.total self.quit.set() log(3, '[PlayerInfo {id}] Event onPlayBackEnded', id=self.thread_id) def onPlayBackError(self): # pylint: disable=invalid-name """Called when playback stops due to an error""" if not self.listen: return self.quit.set() log(3, '[PlayerInfo {id}] Event onPlayBackError', id=self.thread_id) def onPlayBackStopped(self): # pylint: disable=invalid-name """Called when user stops Kodi playing a file""" if not self.listen: return self.quit.set() log(3, '[PlayerInfo {id}] Event onPlayBackStopped', id=self.thread_id) def onPlayerExit(self): # pylint: disable=invalid-name """Called when player exits""" log(3, '[PlayerInfo {id}] Event onPlayerExit', id=self.thread_id) self.positionthread = None self.push_position(position=self.last_pos, total=self.total) # Set property to let wait_for_resumepoints function know that update resume is done set_property('vrtnu_resumepoints', 'ready') def stream_position(self): """Get latest stream position while playing""" while self.isPlaying() and not self.quit.is_set(): self.update_position() if self.quit.wait(timeout=0.2): break self.onPlayerExit() def add_upnext(self, video_id): """Add Up Next url to Kodi Player""" # Reset vrtnu_resumepoints property set_property('vrtnu_resumepoints', None) url = url_for('play_upnext', video_id=video_id) self.update_position() self.update_total() if self.isPlaying() and self.total - self.last_pos < 1: log(3, '[PlayerInfo {id}] Add {url} to Kodi Playlist', id=self.thread_id, url=url) PlayList(1).add(url) else: log(3, '[PlayerInfo {id}] Add {url} to Kodi Player', id=self.thread_id, url=url) self.play(url) def push_upnext(self): """Push episode info to Up Next service add-on""" if has_addon('service.upnext') and get_setting_bool( 'useupnext', default=True) and self.isPlaying(): info_tag = self.getVideoInfoTag() next_info = self.apihelper.get_upnext( dict( program=to_unicode(info_tag.getTVShowTitle()), playcount=info_tag.getPlayCount(), rating=info_tag.getRating(), path=self.path, runtime=self.total, )) if next_info: from base64 import b64encode from json import dumps data = [to_unicode(b64encode(dumps(next_info).encode()))] sender = '{addon_id}.SIGNAL'.format(addon_id=addon_id()) notify(sender=sender, message='upnext_data', data=data) def update_position(self): """Update the player position, when possible""" try: self.last_pos = self.getTime() except RuntimeError: pass def virtualsubclip_seektozero(self): """VRT NU already offers some programs (mostly current affairs programs) as video on demand while the program is still being broadcasted live. To do so, a start timestamp is added to the livestream url so the Unified Origin streaming platform knows it should return a time bounded manifest file that indicates the beginning of the program. This is called a Live-to-VOD stream or virtual subclip: https://docs.unified-streaming.com/documentation/vod/player-urls.html#virtual-subclips e.g. https://live-cf-vrt.akamaized.net/groupc/live/8edf3bdf-7db3-41c3-a318-72cb7f82de66/live.isml/.mpd?t=2020-07-20T11:07:00 For some unclear reason the virtual subclip defined by a single start timestamp still behaves as a ordinary livestream and starts at the live edge of the stream. It seems this is not a Kodi or Inputstream Adaptive bug, because other players like THEOplayer or DASH-IF's reference player treat this kind of manifest files the same way. The only difference is that seeking to the beginning of the program is possible. So if the url contains a single start timestamp, we can work around this problem by automatically seeking to the beginning of the program. """ playing_file = self.getPlayingFile() if any(param in playing_file for param in ('?t=', '&t=')): try: # Python 3 from urllib.parse import parse_qs, urlsplit except ImportError: # Python 2 from urlparse import parse_qs, urlsplit import re # Detect single start timestamp timestamp = parse_qs(urlsplit(playing_file).query).get('t')[0] rgx = re.compile(r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$') is_single_start_timestamp = bool(re.match(rgx, timestamp)) if is_single_start_timestamp: # Check resume status resume_info = jsonrpc(method='Player.GetItem', params=dict(playerid=1, properties=['resume' ])).get('result') if resume_info: resume_position = resume_info.get('item').get( 'resume').get('position') is_resumed = abs(resume_position - self.getTime()) < 1 # Seek to zero if the user didn't resume the program if not is_resumed: log(3, '[PlayerInfo {id}] Virtual subclip: seeking to the beginning of the program', id=self.thread_id) self.seekTime(0) else: log_error( 'Failed to start virtual subclip {playing_file} at start timestamp', playing_file=playing_file) def update_total(self): """Update the total video time""" try: total = self.getTotalTime() # Kodi Player sometimes returns a total time of 0.0 and this causes unwanted behaviour with VRT NU Resumepoints API. if total > 0.0: self.total = total except RuntimeError: pass def push_position(self, position, total): """Push player position to VRT NU resumepoints API and reload container""" # Not all content has an video_id if not self.video_id: return # Push resumepoint to VRT NU self.resumepoints.update_resumepoint(video_id=self.video_id, asset_str=self.asset_str, title=self.title, position=position, total=total, path=self.path, episode_id=self.episode_id, episode_title=self.episode_title)
class TestFavorites(unittest.TestCase): _favorites = Favorites() _resumepoints = ResumePoints() _apihelper = ApiHelper(_favorites, _resumepoints) def test_get_recent_episodes(self): ''' Test items, sort and order ''' episode_items, sort, ascending, content = self._apihelper.list_episodes(page=1, variety='recent') self.assertEqual(len(episode_items), 50) self.assertEqual(sort, 'dateadded') self.assertFalse(ascending) self.assertEqual(content, 'episodes') def test_get_offline_episodes(self): ''' Test items, sort and order ''' episode_items, sort, ascending, content = self._apihelper.list_episodes(page=1, variety='offline') self.assertTrue(episode_items) self.assertEqual(sort, 'dateadded') self.assertFalse(ascending) self.assertEqual(content, 'episodes') @unittest.SkipTest def test_unfollow_all(self): programs = self._apihelper.get_tvshows() for program_item in programs: program_title = program_item.get('title') program = program_item.get('programName') if self._favorites.is_favorite(program): # Unfollow self._favorites.unfollow(program=program, title=program_title) self.assertFalse(self._favorites.is_favorite(program)) @unittest.SkipTest def test_follow_number(self): number = 118 programs = self._apihelper.get_tvshows() shuffle(programs) print('VRT NU has %d programs available' % len(programs)) for program_item in programs[:number]: program_title = program_item.get('title') program = program_item.get('programName') # Follow self._favorites.follow(program=program, title=program_title) self.assertTrue(self._favorites.is_favorite(program)) # Unfollow # self._favorites.unfollow(program=program, title=program_title) # self.assertFalse(self._favorites.is_favorite(program)) @unittest.skipUnless(addon.settings.get('username'), 'Skipping as VRT username is missing.') @unittest.skipUnless(addon.settings.get('password'), 'Skipping as VRT password is missing.') def test_follow_unfollow(self): programs = [ {'program_title': 'Winteruur', 'program': 'winteruur'}, {'program_title': 'De Campus Cup', 'program': 'de-campus-cup'}, {'program_title': 'Terug naar Siberië', 'program': 'terug-naar-siberie'}, {'program_title': 'Belle & Sebastian', 'program': 'belle---sebastian'}, ] for program_item in programs: program_title = program_item.get('program_title') program = program_item.get('program') self._favorites.follow(program=program, title=program_title) self.assertTrue(self._favorites.is_favorite(program)) self._favorites.unfollow(program=program, title=program_title) self.assertFalse(self._favorites.is_favorite(program)) self._favorites.follow(program=program, title=program_title) self.assertTrue(self._favorites.is_favorite(program)) def test_programs(self): programs = self._favorites.programs() # NOTE: Getting favorites requires credentials if addon.settings.get('username') and addon.settings.get('password'): self.assertTrue(programs) print(programs) def test_titles(self): titles = self._favorites.titles() # NOTE: Getting favorites requires credentials if addon.settings.get('username') and addon.settings.get('password'): self.assertTrue(titles) print(sorted(titles))
class TestVRTPlayer(unittest.TestCase): _favorites = Favorites() _resumepoints = ResumePoints() _apihelper = ApiHelper(_favorites, _resumepoints) _vrtplayer = VRTPlayer() def test_show_videos_single_episode_shows_videos(self): program = 'marathonradio' episode_items, sort, ascending, content = self._apihelper.list_episodes( program=program) self.assertTrue(episode_items, msg=program) self.assertEqual(sort, 'dateadded') self.assertFalse(ascending) self.assertEqual(content, 'episodes') self._vrtplayer.show_episodes_menu(program) def test_show_videos_single_season_shows_videos(self): program = 'het-weer' episode_items, sort, ascending, content = self._apihelper.list_episodes( program=program) self.assertTrue(episode_items, msg=program) self.assertEqual(sort, 'dateadded') self.assertFalse(ascending) self.assertEqual(content, 'episodes') self._vrtplayer.show_episodes_menu(program) def test_show_videos_multiple_seasons_shows_videos(self): program = 'pano' episode_items, sort, ascending, content = self._apihelper.list_episodes( program=program) self.assertTrue(episode_items) self.assertEqual(sort, 'label') self.assertFalse(ascending) self.assertEqual(content, 'seasons') self._vrtplayer.show_episodes_menu(program) def test_show_videos_specific_seasons_shows_videos(self): program = 'thuis' episode_items, sort, ascending, content = self._apihelper.list_episodes( program=program) self.assertTrue(episode_items, msg=program) self.assertEqual(sort, 'label') self.assertFalse(ascending) self.assertEqual(content, 'seasons') self._vrtplayer.show_episodes_menu(program) def test_categories_scraping(self): ''' Test to ensure our hardcoded categories conforms to scraped categories ''' # Remove thumbnails from scraped categories first categories_scraped = [ dict(id=c['id'], name=c['name']) for c in self._apihelper.get_categories() ] categories_stored = [ dict(id=c['id'], name=c['name']) for c in CATEGORIES ] self.assertEqual(categories_scraped, categories_stored) def test_random_tvshow_episodes(self): ''' Rest episode from a random tvshow in a random category ''' categories = self._apihelper.get_categories() self.assertTrue(categories) category = random.choice(categories) tvshow_items = self._apihelper.list_tvshows(category['id']) self.assertTrue(tvshow_items, msg=category['id']) tvshow = random.choice(tvshow_items) if tvshow.path.startswith('plugin://plugin.video.vrt.nu/programs/'): # When random program has episodes episode_items, sort, ascending, content = self._apihelper.list_episodes( tvshow.path.split('/')[4].replace('.relevant', '')) self.assertTrue(episode_items, msg=tvshow.path.split('/')[4]) self.assertTrue( sort in ['dateadded', 'episode', 'label', 'unsorted']) self.assertTrue(ascending is True or ascending is False) self.assertTrue( content in ['episodes', 'seasons'], "Content for '%s' is '%s'" % (tvshow.title, content)) elif tvshow.path.startswith('plugin://plugin.video.vrt.nu/play/id/'): # When random program is playable item pass else: self.fail( 'We did not expect this, either we find episodes or it is a playable item' ) def test_categories(self): ''' Test to ensure our hardcoded categories conforms to scraped categories ''' category_items = self._apihelper.list_categories() self.assertEqual(len(category_items), 17) def test_featured(self): ''' Test to ensure our hardcoded categories conforms to scraped categories ''' featured_items = self._apihelper.list_featured() self.assertEqual(len(featured_items), 9) def test_play_unknown_program(self): ''' Test playing latest episode of an unknown program ''' self._vrtplayer.play_latest_episode(program='foobar') def test_play_unknown_airdate(self): ''' Test playing unknown airdate ''' self._vrtplayer.play_episode_by_air_date( channel='een', start_date='2100-01-01T23:59:58', end_date='2100-01-01T23:59:59') self._vrtplayer.play_episode_by_air_date( channel='foo', start_date='2100-01-01T23:59:58', end_date='2100-01-01T23:59:59') def test_play_unknown_whatson_id(self): ''' Test playing unknown whatson id ''' self._vrtplayer.play_whatson(whatson_id='1234567890')
class TestApiHelper(unittest.TestCase): _favorites = Favorites() _resumepoints = ResumePoints() _apihelper = ApiHelper(_favorites, _resumepoints) def test_get_api_data_single_season(self): title_items, sort, ascending, content = self._apihelper.list_episodes( program='het-journaal') self.assertTrue(110 <= len(title_items) <= 140, 'We got %s items instead.' % len(title_items)) self.assertEqual(sort, 'dateadded') self.assertFalse(ascending) self.assertEqual(content, 'episodes') def test_get_api_data_multiple_seasons(self): title_items, sort, ascending, content = self._apihelper.list_episodes( program='thuis') self.assertTrue(len(title_items) < 5) self.assertEqual(sort, 'label') self.assertFalse(ascending) self.assertEqual(content, 'seasons') def test_get_api_data_specific_season(self): title_items, sort, ascending, content = self._apihelper.list_episodes( program='pano') self.assertEqual(len(title_items), 5) self.assertEqual(sort, 'label') self.assertFalse(ascending) self.assertEqual(content, 'seasons') def test_get_api_data_specific_season_without_broadcastdate(self): title_items, sort, ascending, content = self._apihelper.list_episodes( program='postbus-x') self.assertEqual(len(title_items), 4) self.assertEqual(sort, 'label') self.assertTrue(ascending) self.assertEqual(content, 'seasons') def test_get_recent_episodes(self): ''' Test items, sort and order ''' episode_items, sort, ascending, content = self._apihelper.list_episodes( page=1, variety='recent') self.assertEqual(len(episode_items), 50) self.assertEqual(sort, 'dateadded') self.assertFalse(ascending) self.assertEqual(content, 'episodes') def test_get_recent_episodes_page1(self): ''' Test items, sort and order ''' episode_items, sort, ascending, content = self._apihelper.list_episodes( page=2, variety='recent') self.assertEqual(len(episode_items), 50) self.assertEqual(sort, 'dateadded') self.assertFalse(ascending) self.assertEqual(content, 'episodes') def test_get_recent_episodes_page2(self): ''' Test items, sort and order ''' episode_items, sort, ascending, content = self._apihelper.list_episodes( page=3, variety='recent') self.assertEqual(len(episode_items), 50) self.assertEqual(sort, 'dateadded') self.assertFalse(ascending) self.assertEqual(content, 'episodes') def test_get_offline_episodes(self): ''' Test items, sort and order ''' episode_items, sort, ascending, content = self._apihelper.list_episodes( page=1, variety='offline') self.assertTrue(episode_items) self.assertEqual(sort, 'dateadded') self.assertFalse(ascending) self.assertEqual(content, 'episodes') def test_get_tvshows(self): ''' Test get tvshows ''' category = 'humor' tvshow_items = self._apihelper.get_tvshows(category=category) self.assertTrue(tvshow_items) def test_list_tvshows(self): ''' Test items, sort and order ''' category = 'nieuws-en-actua' tvshow_items = self._apihelper.list_tvshows(category=category) self.assertTrue(tvshow_items) def test_tvshows(self): ''' Test A-Z tvshow listing and CHANNELS list ''' tvshow_items = self._apihelper.list_tvshows(category=None) # Test we get a non-empty A-Z listing back self.assertTrue(tvshow_items) # Test every brand is a known channel studio name bogus_brands = ['lang-zullen-we-lezen', 'VRT'] channel_studios = [c.get('studio') for c in CHANNELS] + bogus_brands for tvshow in tvshow_items: self.assertTrue( tvshow.info_dict['studio'] in channel_studios, '%s | %s | %s' % (tvshow.title, tvshow.info_dict['studio'], channel_studios)) def test_get_latest_episode(self): video = self._apihelper.get_latest_episode(program='het-journaal') self.assertTrue(video.get('listitem') is not None) self.assertTrue(video.get('video_id') is not None) self.assertTrue(video.get('publication_id') is not None) def test_episode_plot(self): title_items, sort, ascending, content = self._apihelper.list_episodes( program='thuis', season='allseasons') self.assertEqual(sort, 'dateadded') self.assertFalse(ascending) self.assertEqual(content, 'episodes') plot = title_items[0].info_dict['plot'] print(kodi_to_ansi(plot)) def test_upnext(self): current_episode = dict( program='winteruur', path='plugin://plugin.video.vrt.nu/play/whatson/705308178527', playcount='0', rating='0', runtime='600', ) next_episode = self._apihelper.get_upnext(info=current_episode) print(next_episode)
class PlayerInfo(Player, object): # pylint: disable=useless-object-inheritance """Class for communication with Kodi player""" def __init__(self): """PlayerInfo initialisation""" self.resumepoints = ResumePoints() self.apihelper = ApiHelper(Favorites(), self.resumepoints) self.last_pos = None self.listen = False self.paused = False self.total = 100 self.positionthread = None self.quit = Event() self.asset_id = None # FIXME On Kodi 17, use ListItem.Filenameandpath because Player.FilenameAndPath returns the stream manifest url and # this definitely breaks "Up Next" on Kodi 17, but this is not supported or available through the Kodi add-on repo anyway self.path_infolabel = 'ListItem.Filenameandpath' if kodi_version_major( ) < 18 else 'Player.FilenameAndPath' self.path = None self.title = None self.ep_id = None self.url = None self.whatson_id = None from random import randint self.thread_id = randint(1, 10001) log(3, '[PlayerInfo {id}] Initialized', id=self.thread_id) super(PlayerInfo, self).__init__() def onPlayBackStarted(self): # pylint: disable=invalid-name """Called when user starts playing a file""" self.path = getInfoLabel(self.path_infolabel) if self.path.startswith('plugin://plugin.video.vrt.nu/'): self.listen = True else: self.listen = False return log(3, '[PlayerInfo {id}] Event onPlayBackStarted', id=self.thread_id) # Set property to let wait_for_resumepoints function know that update resume is busy set_property('vrtnu_resumepoints', 'busy') # Update previous episode when using "Up Next" if self.path.startswith('plugin://plugin.video.vrt.nu/play/upnext'): self.push_position(position=self.last_pos, total=self.total) # Reset episode data self.asset_id = None self.title = None self.url = None self.whatson_id = None ep_id = play_url_to_id(self.path) # Avoid setting resumepoints for livestreams for channel in CHANNELS: if ep_id.get('video_id') and ep_id.get('video_id') == channel.get( 'live_stream_id'): log(3, '[PlayerInfo {id}] Avoid setting resumepoints for livestream {video_id}', id=self.thread_id, video_id=ep_id.get('video_id')) self.listen = False # Reset vrtnu_resumepoints property before return set_property('vrtnu_resumepoints', None) return # Get episode data needed to update resumepoints from VRT NU Search API episode = self.apihelper.get_single_episode_data( video_id=ep_id.get('video_id'), whatson_id=ep_id.get('whatson_id'), video_url=ep_id.get('video_url')) # Avoid setting resumepoints without episode data if episode is None: # Reset vrtnu_resumepoints property before return set_property('vrtnu_resumepoints', None) return from metadata import Metadata self.asset_id = Metadata(None, None).get_asset_id(episode) self.title = episode.get('program') self.url = url_to_episode(episode.get('url', '')) self.whatson_id = episode.get( 'whatsonId') or None # Avoid empty string # Kodi 17 doesn't have onAVStarted if kodi_version_major() < 18: self.onAVStarted() def onAVStarted(self): # pylint: disable=invalid-name """Called when Kodi has a video or audiostream""" if not self.listen: return log(3, '[PlayerInfo {id}] Event onAVStarted', id=self.thread_id) self.quit.clear() self.update_position() self.update_total() self.push_upnext() # StreamPosition thread keeps running when watching multiple episode with "Up Next" # only start StreamPosition thread when it doesn't exist yet. if not self.positionthread: self.positionthread = Thread(target=self.stream_position, name='StreamPosition') self.positionthread.start() def onAVChange(self): # pylint: disable=invalid-name """Called when Kodi has a video, audio or subtitle stream. Also happens when the stream changes.""" def onPlayBackSeek(self, time, seekOffset): # pylint: disable=invalid-name """Called when user seeks to a time""" if not self.listen: return log(3, '[PlayerInfo {id}] Event onPlayBackSeek time={time} offset={offset}', id=self.thread_id, time=time, offset=seekOffset) self.last_pos = time // 1000 # If we seek beyond the end, exit Player if self.last_pos >= self.total: self.quit.set() self.stop() def onPlayBackPaused(self): # pylint: disable=invalid-name """Called when user pauses a playing file""" if not self.listen: return log(3, '[PlayerInfo {id}] Event onPlayBackPaused', id=self.thread_id) self.update_position() self.push_position(position=self.last_pos, total=self.total) self.paused = True def onPlayBackResumed(self): # pylint: disable=invalid-name """Called when user resumes a paused file or a next playlist item is started""" if not self.listen: return suffix = 'after pausing' if self.paused else 'after playlist change' log(3, '[PlayerInfo {id}] Event onPlayBackResumed {suffix}', id=self.thread_id, suffix=suffix) self.paused = False def onPlayBackEnded(self): # pylint: disable=invalid-name """Called when Kodi has ended playing a file""" if not self.listen: return self.last_pos = self.total self.quit.set() log(3, '[PlayerInfo {id}] Event onPlayBackEnded', id=self.thread_id) def onPlayBackError(self): # pylint: disable=invalid-name """Called when playback stops due to an error""" if not self.listen: return self.quit.set() log(3, '[PlayerInfo {id}] Event onPlayBackError', id=self.thread_id) def onPlayBackStopped(self): # pylint: disable=invalid-name """Called when user stops Kodi playing a file""" if not self.listen: return self.quit.set() log(3, '[PlayerInfo {id}] Event onPlayBackStopped', id=self.thread_id) def onPlayerExit(self): # pylint: disable=invalid-name """Called when player exits""" log(3, '[PlayerInfo {id}] Event onPlayerExit', id=self.thread_id) self.positionthread = None self.push_position(position=self.last_pos, total=self.total) # Set property to let wait_for_resumepoints function know that update resume is done set_property('vrtnu_resumepoints', 'ready') def stream_position(self): """Get latest stream position while playing""" while self.isPlaying() and not self.quit.is_set(): self.update_position() if self.quit.wait(timeout=0.2): break self.onPlayerExit() def add_upnext(self, video_id): """Add Up Next url to Kodi Player""" # Reset vrtnu_resumepoints property set_property('vrtnu_resumepoints', None) url = 'plugin://plugin.video.vrt.nu/play/upnext/{video_id}'.format( video_id=video_id) self.update_position() self.update_total() if self.isPlaying() and self.total - self.last_pos < 1: log(3, '[PlayerInfo {id}] Add {url} to Kodi Playlist', id=self.thread_id, url=url) PlayList(1).add(url) else: log(3, '[PlayerInfo {id}] Add {url} to Kodi Player', id=self.thread_id, url=url) self.play(url) def push_upnext(self): """Push episode info to Up Next service add-on""" if has_addon('service.upnext') and get_setting_bool( 'useupnext', default=True) and self.isPlaying(): info_tag = self.getVideoInfoTag() next_info = self.apihelper.get_upnext( dict( program=to_unicode(info_tag.getTVShowTitle()), playcount=info_tag.getPlayCount(), rating=info_tag.getRating(), path=self.path, runtime=self.total, )) if next_info: from base64 import b64encode from json import dumps data = [to_unicode(b64encode(dumps(next_info).encode()))] sender = '{addon_id}.SIGNAL'.format(addon_id=addon_id()) notify(sender=sender, message='upnext_data', data=data) def update_position(self): """Update the player position, when possible""" try: self.last_pos = self.getTime() except RuntimeError: pass def update_total(self): """Update the total video time""" try: total = self.getTotalTime() # Kodi Player sometimes returns a total time of 0.0 and this causes unwanted behaviour with VRT NU Resumepoints API. if total > 0.0: self.total = total except RuntimeError: pass def push_position(self, position, total): """Push player position to VRT NU resumepoints API and reload container""" # Not all content has an asset_id if not self.asset_id: return # Push resumepoint to VRT NU self.resumepoints.update(asset_id=self.asset_id, title=self.title, url=self.url, position=position, total=total, whatson_id=self.whatson_id, path=self.path)
class VrtMonitor(Monitor): ''' This is the class that monitors Kodi for the VRT NU video plugin ''' def __init__(self): ''' VRT Monitor initialisiation ''' self._resumepoints = ResumePoints() self._container = None self._playerinfo = None self._favorites = None self._apihelper = None self.init_watching_activity() Monitor.__init__(self) def run(self): ''' Main loop ''' while not self.abortRequested(): if self.waitForAbort(10): break def init_watching_activity(self): ''' Only load components for watching activity when needed ''' if self._resumepoints.is_activated(): if not self._playerinfo: self._playerinfo = PlayerInfo(info=self.handle_info) if not self._favorites: self._favorites = Favorites() if not self._apihelper: self._apihelper = ApiHelper(self._favorites, self._resumepoints) def onNotification(self, sender, method, data): # pylint: disable=invalid-name ''' Handler for notifications ''' log(2, '[Notification] sender={sender}, method={method}, data={data}', sender=sender, method=method, data=to_unicode(data)) if method.endswith('source_container'): from json import loads self._container = loads(data).get('container') return if not sender.startswith('upnextprovider'): return if not method.endswith('plugin.video.vrt.nu_play_action'): return from json import loads hexdata = loads(data) if not hexdata: return from binascii import unhexlify data = loads(unhexlify(hexdata[0])) log(2, '[Up Next notification] sender={sender}, method={method}, data={data}', sender=sender, method=method, data=to_unicode(data)) jsonrpc(method='Player.Open', params=dict(item=dict( file='plugin://plugin.video.vrt.nu/play/whatson/%s' % data.get('whatson_id')))) def onSettingsChanged(self): # pylint: disable=invalid-name ''' Handler for changes to settings ''' log(1, 'Settings changed') TokenResolver().refresh_login() invalidate_caches('continue-*.json', 'favorites.json', 'my-offline-*.json', 'my-recent-*.json', 'resume_points.json', 'watchlater-*.json') # Init watching activity again when settings change self.init_watching_activity() # Refresh container when settings change container_refresh() def handle_info(self, info): ''' Handle information from PlayerInfo class ''' log(2, 'Got VRT NU Player info: {info}', info=str(info)) # Push resume position if info.get('position'): self.push_position(info) # Push up next episode info if info.get('program'): self.push_upnext(info) def push_position(self, info): ''' Push player position to VRT NU resumepoints API ''' # Get uuid, title and url from api based on video.get('publication_id') or video.get('video_url') ep_id = play_url_to_id(info.get('path')) if ep_id.get('video_id'): episode = self._apihelper.get_episodes( video_id=ep_id.get('video_id'), variety='single')[0] elif ep_id.get('whatson_id'): episode = self._apihelper.get_episodes( whatson_id=ep_id.get('whatson_id'), variety='single')[0] elif ep_id.get('video_url'): episode = self._apihelper.get_episodes( video_url=ep_id.get('video_url'), variety='single')[0] uuid = self._resumepoints.assetpath_to_uuid(episode.get('assetPath')) title = episode.get('program') url = url_to_episode(episode.get('url', '')) # Push resumepoint to VRT NU self._resumepoints.update(uuid=uuid, title=title, url=url, watch_later=None, position=info.get('position'), total=info.get('total')) # Only update container if the play action was initiated from it current_container = current_container_url() log(2, '[PlayerPosition] resumepoint update {info} {container}', info=episode.get('title'), container=current_container) if current_container is None or self._container == current_container: log(2, '[PlayerPosition] update container {info}', info=self._container) container_update(self._container) def push_upnext(self, info): ''' Push episode info to Up Next service add-on''' if has_addon('service.upnext') and get_setting('useupnext', 'true') == 'true': next_info = self._apihelper.get_upnext(info) if next_info: from binascii import hexlify from json import dumps data = [to_unicode(hexlify(dumps(next_info).encode()))] sender = '%s.SIGNAL' % addon_id() notify(sender=sender, message='upnext_data', data=data)