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__(self):
     """VRT Monitor initialisation"""
     self._resumepoints = ResumePoints()
     self._playerinfo = None
     self._favorites = None
     self._apihelper = None
     self.init_watching_activity()
     super(VrtMonitor, self).__init__()
예제 #3
0
 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)
예제 #4
0
def unwatchlater(uuid, title, url):
    ''' The API interface to unwatch an episode used by the context menu '''
    from resumepoints import ResumePoints
    ResumePoints().unwatchlater(uuid=uuid,
                                title=to_unicode(
                                    unquote_plus(from_unicode(title))),
                                url=url)
예제 #5
0
def watchlater(asset_id, title, url):
    """The API interface to watch an episode used by the context menu"""
    from resumepoints import ResumePoints
    ResumePoints().watchlater(asset_id=asset_id,
                              title=to_unicode(
                                  unquote_plus(from_unicode(title))),
                              url=url)
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))
예제 #7
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='')
예제 #8
0
def resumepoints_refresh():
    ''' The API interface to refresh the resumepoints cache '''
    from resumepoints import ResumePoints
    ResumePoints().refresh(ttl=0)
    notification(message=localize(30983))
예제 #9
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)
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)
예제 #11
0
 def __init__(self):
     """Initialize searchtes, relies on XBMC vfs"""
     self._favorites = Favorites()
     self._resumepoints = ResumePoints()
     self._search_history = addon_profile() + 'search_history.json'
예제 #12
0
def unwatchlater(episode_id, title):
    """The API interface to unwatch an episode used by the context menu"""
    from resumepoints import ResumePoints
    ResumePoints().unwatchlater(episode_id=episode_id,
                                title=to_unicode(
                                    unquote_plus(from_unicode(title))))
예제 #13
0
 def __init__(self):
     ''' Initialise object '''
     self._favorites = Favorites()
     self._resumepoints = ResumePoints()
     self._apihelper = ApiHelper(self._favorites, self._resumepoints)
예제 #14
0
class VrtMonitor(Monitor, object):  # pylint: disable=useless-object-inheritance
    """This is the class that monitors Kodi for the VRT NU video plugin"""
    def __init__(self):
        """VRT Monitor initialisation"""
        self._resumepoints = ResumePoints()
        self._playerinfo = None
        self._favorites = None
        self._apihelper = None
        self.init_watching_activity()
        super(VrtMonitor, self).__init__()

    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()
            if not self._favorites:
                self._favorites = Favorites()
            if not self._apihelper:
                self._apihelper = ApiHelper(self._favorites,
                                            self._resumepoints)
        else:
            self._playerinfo = None

    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))

        # Handle play_action events from upnextprovider
        if sender.startswith('upnextprovider') and method.endswith(
                'plugin.video.vrt.nu_play_action'):
            from json import loads
            hexdata = loads(data)

            if not hexdata:
                return

            # NOTE: With Python 3.5 and older json.loads() does not support bytes or bytearray, so we convert to unicode
            from base64 import b64decode
            data = loads(to_unicode(b64decode(hexdata[0])))
            log(2,
                '[Up Next notification] sender={sender}, method={method}, data={data}',
                sender=sender,
                method=method,
                data=to_unicode(data))
            self._playerinfo.add_upnext(data.get('video_id'))

    def onSettingsChanged(self):  # pylint: disable=invalid-name
        """Handler for changes to settings"""

        log(1, 'Settings changed')
        TokenResolver().refresh_login()

        # Init watching activity again when settings change
        self.init_watching_activity()

        # Refresh container when settings change
        container_refresh()
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)
예제 #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)
예제 #17
0
 def __init__(self):
     ''' Initialize searchtes, relies on XBMC vfs '''
     self._favorites = Favorites()
     self._resumepoints = ResumePoints()
     self._search_history = get_userdata_path() + 'search_history.json'
예제 #18
0
 def __init__(self):
     """Initializes TV-guide object"""
     self._favorites = Favorites()
     self._resumepoints = ResumePoints()
     self._metadata = Metadata(self._favorites, self._resumepoints)
예제 #19
0
class TVGuide:
    """This implements a VRT TV-guide that offers Kodi menus and TV guide info"""

    VRT_TVGUIDE = 'https://www.vrt.be/bin/epg/schedule.%Y-%m-%d.json'

    def __init__(self):
        """Initializes TV-guide object"""
        self._favorites = Favorites()
        self._resumepoints = ResumePoints()
        self._metadata = Metadata(self._favorites, self._resumepoints)

    def show_tvguide(self, date=None, channel=None):
        """Offer a menu depending on the information provided"""

        if not date and not channel:
            date_items = self.get_date_items()
            show_listing(date_items, category=30026,
                         content='files')  # TV guide

        elif not channel:
            channel_items = self.get_channel_items(date=date)
            entry = find_entry(RELATIVE_DATES, 'id', date)
            date_name = localize(entry.get('msgctxt')) if entry else date
            show_listing(channel_items, category=date_name)

        elif not date:
            date_items = self.get_date_items(channel=channel)
            channel_name = find_entry(CHANNELS, 'name', channel).get('label')
            show_listing(date_items,
                         category=channel_name,
                         content='files',
                         selected=7)

        else:
            episode_items = self.get_episode_items(date, channel)
            channel_name = find_entry(CHANNELS, 'name', channel).get('label')
            entry = find_entry(RELATIVE_DATES, 'id', date)
            date_name = localize(entry.get('msgctxt')) if entry else date
            show_listing(episode_items,
                         category='%s / %s' % (channel_name, date_name),
                         content='episodes',
                         cache=False)

    @staticmethod
    def get_date_items(channel=None):
        """Offer a menu to select the TV-guide date"""

        epg = datetime.now(dateutil.tz.tzlocal())
        # Daily EPG information shows information from 6AM until 6AM
        if epg.hour < 6:
            epg += timedelta(days=-1)
        date_items = []
        for offset in range(14, -19, -1):
            day = epg + timedelta(days=offset)
            label = localize_datelong(day)
            date = day.strftime('%Y-%m-%d')

            # Highlight today with context of 2 days
            entry = find_entry(RELATIVE_DATES, 'offset', offset)
            if entry:
                date_name = localize(entry.get('msgctxt'))
                if entry.get('permalink'):
                    date = entry.get('id')
                if offset == 0:
                    label = '[COLOR={highlighted}][B]{name}[/B], {date}[/COLOR]'.format(
                        highlighted=themecolour('highlighted'),
                        name=date_name,
                        date=label)
                else:
                    label = '[B]{name}[/B], {date}'.format(name=date_name,
                                                           date=label)

            plot = '[B]{datelong}[/B]'.format(datelong=localize_datelong(day))

            # Show channel list or channel episodes
            if channel:
                path = url_for('tvguide', date=date, channel=channel)
            else:
                path = url_for('tvguide', date=date)

            cache_file = 'schedule.{date}.json'.format(date=date)
            date_items.append(
                TitleItem(
                    label=label,
                    path=path,
                    art_dict=dict(thumb='DefaultYear.png'),
                    info_dict=dict(plot=plot),
                    context_menu=[(
                        localize(30413),  # Refresh menu
                        'RunPlugin(%s)' %
                        url_for('delete_cache', cache_file=cache_file))],
                ))
        return date_items

    def get_channel_items(self, date=None, channel=None):
        """Offer a menu to select the channel"""
        if date:
            now = datetime.now(dateutil.tz.tzlocal())
            epg = self.parse(date, now)
            datelong = localize_datelong(epg)

        channel_items = []
        for chan in CHANNELS:
            # Only some channels are supported
            if not chan.get('has_tvguide'):
                continue

            # If a channel is requested, stop processing if it is no match
            if channel and channel != chan.get('name'):
                continue

            art_dict = {}

            # Try to use the white icons for thumbnails (used for icons as well)
            if has_addon('resource.images.studios.white'):
                art_dict[
                    'thumb'] = 'resource://resource.images.studios.white/{studio}.png'.format(
                        **chan)
            else:
                art_dict['thumb'] = 'DefaultTags.png'

            if date:
                label = chan.get('label')
                path = url_for('tvguide', date=date, channel=chan.get('name'))
                plot = '[B]%s[/B]\n%s' % (datelong, localize(30302, **chan))
            else:
                label = '[B]%s[/B]' % localize(30303, **chan)
                path = url_for('tvguide_channel', channel=chan.get('name'))
                plot = '%s\n\n%s' % (localize(
                    30302, **chan), self.live_description(chan.get('name')))

            context_menu = [(
                localize(30413),  # Refresh menu
                'RunPlugin(%s)' %
                url_for('delete_cache',
                        cache_file='channel.{channel}.json'.format(
                            channel=chan.get('name'))),
            )]

            channel_items.append(
                TitleItem(
                    label=label,
                    path=path,
                    art_dict=art_dict,
                    context_menu=context_menu,
                    info_dict=dict(plot=plot, studio=chan.get('studio')),
                ))
        return channel_items

    def get_episode_items(self, date, channel):
        """Show episodes for a given date and channel"""
        now = datetime.now(dateutil.tz.tzlocal())
        epg = self.parse(date, now)
        epg_url = epg.strftime(self.VRT_TVGUIDE)

        self._favorites.refresh(ttl=ttl('indirect'))
        self._resumepoints.refresh(ttl=ttl('indirect'))

        cache_file = 'schedule.{date}.json'.format(date=date)
        if date in ('today', 'yesterday', 'tomorrow'):
            schedule = get_cached_url_json(url=epg_url,
                                           cache=cache_file,
                                           ttl=ttl('indirect'),
                                           fail={})
        else:
            schedule = get_url_json(url=epg_url, fail={})

        entry = find_entry(CHANNELS, 'name', channel)
        if entry:
            episodes = schedule.get(entry.get('id'), [])
        else:
            episodes = []
        episode_items = []
        for episode in episodes:
            program = url_to_program(episode.get('url', ''))
            context_menu, favorite_marker, watchlater_marker = self._metadata.get_context_menu(
                episode, program, cache_file)
            label = self._metadata.get_label(episode)
            path = self.get_episode_path(episode, channel)
            # Playable item
            if '/play/' in path:
                is_playable = True
                label += favorite_marker + watchlater_marker
            # Non-actionable item
            else:
                is_playable = False
                label = '[COLOR={greyedout}]%s[/COLOR]' % label

            # Now playing
            start_date = dateutil.parser.parse(episode.get('startTime'))
            end_date = dateutil.parser.parse(episode.get('endTime'))
            if start_date <= now <= end_date:
                if is_playable:
                    label = '[COLOR={highlighted}]%s[/COLOR] %s' % (
                        label, localize(30301))
                else:
                    label += localize(30301)

            info_labels = self._metadata.get_info_labels(episode,
                                                         date=date,
                                                         channel=entry)
            # FIXME: Due to a bug in Kodi, ListItem.Title is used when Sort methods are used, not ListItem.Label
            info_labels['title'] = colour(label)

            episode_items.append(
                TitleItem(
                    label=colour(label),
                    path=path,
                    art_dict=self._metadata.get_art(episode),
                    info_dict=info_labels,
                    context_menu=context_menu,
                    is_playable=is_playable,
                ))
        return episode_items

    @staticmethod
    def get_episode_path(episode, channel):
        """Return a playable plugin:// path for an episode"""
        now = datetime.now(dateutil.tz.tzlocal())
        end_date = dateutil.parser.parse(episode.get('endTime'))
        if episode.get('url') and episode.get('vrt.whatson-id'):
            return url_for('play_whatson_id',
                           whatson_id=episode.get('vrt.whatson-id'))
        if now - timedelta(hours=24) <= end_date <= now:
            return url_for('play_air_date', channel,
                           episode.get('startTime')[:19],
                           episode.get('endTime')[:19])
        return url_for('noop', whatsonid=episode.get('vrt.whatson-id', ''))

    def get_epg_data(self):
        """Return EPG data"""
        now = datetime.now(dateutil.tz.tzlocal())

        epg_data = {}
        for date in ['yesterday', 'today', 'tomorrow']:
            epg = self.parse(date, now)
            epg_url = epg.strftime(self.VRT_TVGUIDE)
            schedule = get_url_json(url=epg_url, fail={})
            for channel_id, episodes in list(schedule.items()):
                channel = find_entry(CHANNELS, 'id', channel_id)
                epg_id = channel.get('epg_id')
                if epg_id not in epg_data:
                    epg_data[epg_id] = []
                for episode in episodes:
                    if episode.get('url') and episode.get('vrt.whatson-id'):
                        path = url_for(
                            'play_whatson_id',
                            whatson_id=episode.get('vrt.whatson-id'))
                    else:
                        path = None
                    epg_data[epg_id].append(
                        dict(
                            start=episode.get('startTime'),
                            stop=episode.get('endTime'),
                            image=add_https_proto(episode.get('image', '')),
                            title=episode.get('title'),
                            subtitle=html_to_kodi(episode.get('subtitle', '')),
                            description=html_to_kodi(
                                episode.get('description', '')),
                            stream=path,
                        ))
        return epg_data

    def playing_now(self, channel):
        """Return the EPG information for what is playing now"""
        now = datetime.now(dateutil.tz.tzlocal())
        epg = now
        # Daily EPG information shows information from 6AM until 6AM
        if epg.hour < 6:
            epg += timedelta(days=-1)

        entry = find_entry(CHANNELS, 'name', channel)
        if not entry:
            return ''

        epg_url = epg.strftime(self.VRT_TVGUIDE)
        schedule = get_cached_url_json(url=epg_url,
                                       cache='schedule.today.json',
                                       ttl=ttl('indirect'),
                                       fail={})
        episodes = iter(schedule.get(entry.get('id'), []))

        while True:
            try:
                episode = next(episodes)
            except StopIteration:
                break
            start_date = dateutil.parser.parse(episode.get('startTime'))
            end_date = dateutil.parser.parse(episode.get('endTime'))
            if start_date <= now <= end_date:  # Now playing
                return episode.get('title')
        return ''

    @staticmethod
    def episode_description(episode):
        """Return a formatted description for an episode"""
        return '{start} - {end}\n» {title}'.format(**episode)

    def live_description(self, channel):
        """Return the EPG information for current and next live program"""
        now = datetime.now(dateutil.tz.tzlocal())
        epg = now
        # Daily EPG information shows information from 6AM until 6AM
        if epg.hour < 6:
            epg += timedelta(days=-1)

        entry = find_entry(CHANNELS, 'name', channel)
        if not entry:
            return ''

        epg_url = epg.strftime(self.VRT_TVGUIDE)
        schedule = get_cached_url_json(url=epg_url,
                                       cache='schedule.today.json',
                                       ttl=ttl('indirect'),
                                       fail={})
        episodes = iter(schedule.get(entry.get('id'), []))

        description = ''
        episode = None
        while True:
            try:
                episode = next(episodes)
            except StopIteration:
                break
            start_date = dateutil.parser.parse(episode.get('startTime'))
            end_date = dateutil.parser.parse(episode.get('endTime'))
            if start_date <= now <= end_date:  # Now playing
                description = '[COLOR={highlighted}][B]%s[/B] %s[/COLOR]\n' % (
                    localize(30421), self.episode_description(episode))
                try:
                    description += '[B]%s[/B] %s' % (localize(30422),
                                                     self.episode_description(
                                                         next(episodes)))
                except StopIteration:
                    break
                break
            if now < start_date:  # Nothing playing now, but this may be next
                description = '[B]%s[/B] %s\n' % (
                    localize(30422), self.episode_description(episode))
                try:
                    description += '[B]%s[/B] %s' % (localize(30422),
                                                     self.episode_description(
                                                         next(episodes)))
                except StopIteration:
                    break
                break
        if episode and not description:
            # Add a final 'No transmission' program
            description = '[COLOR={highlighted}][B]%s[/B] %s - 06:00\n» %s[/COLOR]' % (
                localize(30421), episode.get('end'), localize(30423))
        return colour(description)

    @staticmethod
    def parse(date, now):
        """Parse a given string and return a datetime object
            This supports 'today', 'yesterday' and 'tomorrow'
            It also compensates for TV-guides covering from 6AM to 6AM
       """
        entry = find_entry(RELATIVE_DATES, 'id', date)
        if not entry:
            return dateutil.parser.parse(date)

        offset = entry.get('offset')
        if now.hour < 6:
            return now + timedelta(days=offset - 1)

        return now + timedelta(days=offset)
예제 #20
0
 def __init__(self):
     ''' Initializes TV-guide object '''
     self._favorites = Favorites()
     self._resumepoints = ResumePoints()
     self._metadata = Metadata(self._favorites, self._resumepoints)
     install_opener(build_opener(ProxyHandler(get_proxies())))
예제 #21
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'))
예제 #22
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')
예제 #23
0
 def __init__(self):
     """Initialise object"""
     self._favorites = Favorites()
     self._resumepoints = ResumePoints()
     self._apihelper = ApiHelper(self._favorites, self._resumepoints)
     wait_for_resumepoints()
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))