コード例 #1
0
    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__()
コード例 #2
0
    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)
コード例 #3
0
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))
コード例 #4
0
    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)
コード例 #5
0
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='')
コード例 #6
0
    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
コード例 #7
0
    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)
コード例 #8
0
 def __init__(self):
     """Initialise object"""
     self._favorites = Favorites()
     self._resumepoints = ResumePoints()
     self._apihelper = ApiHelper(self._favorites, self._resumepoints)
     wait_for_resumepoints()
コード例 #9
0
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'))
コード例 #10
0
 def __init__(self):
     ''' Initialise object '''
     self._favorites = Favorites()
     self._resumepoints = ResumePoints()
     self._apihelper = ApiHelper(self._favorites, self._resumepoints)
コード例 #11
0
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)
コード例 #12
0
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))
コード例 #13
0
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')
コード例 #14
0
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)
コード例 #15
0
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)
コード例 #16
0
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)