def __init__(self): Monitor.__init__(self) self.update_interval = 24 * 3600 # Every 24 hours self.cache_expiry = 30 * 24 * 3600 # One month self._auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password'), kodiutils.get_tokens_path())
def __init__(self): """ Initialise object """ self._auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password'), kodiutils.get_tokens_path()) self._api = ContentApi(self._auth, cache_path=kodiutils.get_cache_path())
def __init__(self): """ Initialise object """ auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password'), kodiutils.get_tokens_path()) self._api = ContentApi(auth, cache_path=kodiutils.get_cache_path()) # Workaround for Raspberry Pi 3 and older kodiutils.set_global_setting('videoplayer.useomxplayer', True)
def _resolve_stream(uuid): """ Resolve the stream for the requested item :type uuid: string """ try: # Check if we have credentials if not kodiutils.get_setting('username') or not kodiutils.get_setting('password'): confirm = kodiutils.yesno_dialog( message=kodiutils.localize(30701)) # To watch a video, you need to enter your credentials. Do you want to enter them now? if confirm: kodiutils.open_settings() kodiutils.end_of_directory() return None # Fetch an auth token now try: auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password'), kodiutils.get_tokens_path()) # Get stream information resolved_stream = ContentApi(auth).get_stream_by_uuid(uuid) return resolved_stream except (InvalidLoginException, AuthenticationException) as ex: _LOGGER.exception(ex) kodiutils.ok_dialog(message=kodiutils.localize(30702, error=str(ex))) kodiutils.end_of_directory() return None except GeoblockedException: kodiutils.ok_dialog(message=kodiutils.localize(30710)) # This video is geo-blocked... return None except UnavailableException: kodiutils.ok_dialog(message=kodiutils.localize(30712)) # The video is unavailable... return None
class BackgroundService(Monitor): """ Background service code """ def __init__(self): Monitor.__init__(self) self.update_interval = 24 * 3600 # Every 24 hours self.cache_expiry = 30 * 24 * 3600 # One month self._auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password'), kodiutils.get_tokens_path()) self._kodiplayer = KodiPlayer() def run(self): """ Background loop for maintenance tasks """ _LOGGER.debug('Service started') while not self.abortRequested(): # Stop when abort requested if self.waitForAbort(10): break _LOGGER.debug('Service stopped') def onSettingsChanged(self): # pylint: disable=invalid-name """ Callback when a setting has changed """ if self._has_credentials_changed(): _LOGGER.debug('Clearing auth tokens due to changed credentials') self._auth.clear_tokens() # Refresh container kodiutils.container_refresh() @staticmethod def _has_credentials_changed(): """ Check if credentials have changed """ old_hash = kodiutils.get_setting('credentials_hash') new_hash = '' if kodiutils.get_setting('username') or kodiutils.get_setting( 'password'): new_hash = hashlib.md5((kodiutils.get_setting('username') + kodiutils.get_setting('password') ).encode('utf-8')).hexdigest() if new_hash != old_hash: kodiutils.set_setting('credentials_hash', new_hash) return True return False
class TestAuth(unittest.TestCase): def __init__(self, *args, **kwargs): super(TestAuth, self).__init__(*args, **kwargs) self._auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password'), kodiutils.get_tokens_path()) def test_login(self): # Clear any cache we have self._auth.clear_cache() # We should get a token by logging in token = self._auth.get_token() self.assertTrue(token) # Test it a second time, it should go from memory now token = self._auth.get_token() self.assertTrue(token)
class TestApi(unittest.TestCase): def __init__(self, *args, **kwargs): super(TestApi, self).__init__(*args, **kwargs) self._auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password'), kodiutils.get_tokens_path()) def test_notifications(self): api = ContentApi(self._auth.get_token()) notifications = api.get_notifications() self.assertIsInstance(notifications, list) def test_programs(self): api = ContentApi(self._auth.get_token()) for channel in ['vier', 'vijf', 'zes']: channels = api.get_programs(channel) self.assertIsInstance(channels, list) def test_episodes(self): api = ContentApi(self._auth.get_token()) for channel, program in [('vier', 'auwch'), ('vijf', 'zo-man-zo-vrouw')]: program = api.get_program(channel, program) self.assertIsInstance(program, Program) self.assertIsInstance(program.seasons, dict) # self.assertIsInstance(program.seasons[0], Season) self.assertIsInstance(program.episodes, list) self.assertIsInstance(program.episodes[0], Episode) _LOGGER.info('Got program: %s', program) def test_get_stream(self): api = ContentApi(self._auth.get_token()) program = api.get_program('vier', 'auwch') episode = program.episodes[0] video = api.get_stream(episode.channel, episode.uuid) self.assertTrue(video) _LOGGER.info('Got video URL: %s', video)
def play(item): """ Play the requested item. :type item: string """ # Workaround for Raspberry Pi 3 and older omxplayer = kodiutils.get_global_setting('videoplayer.useomxplayer') if omxplayer is False: kodiutils.set_global_setting('videoplayer.useomxplayer', True) try: # Check if we have credentials if not kodiutils.get_setting( 'username') or not kodiutils.get_setting('password'): confirm = kodiutils.yesno_dialog( message=kodiutils.localize(30701) ) # To watch a video, you need to enter your credentials. Do you want to enter them now? if confirm: kodiutils.open_settings() kodiutils.end_of_directory() return # Fetch an auth token now try: auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password'), kodiutils.get_tokens_path()) # Get stream information resolved_stream = ContentApi(auth).get_stream_by_uuid(item) except (InvalidLoginException, AuthenticationException) as ex: _LOGGER.error(ex) kodiutils.ok_dialog( message=kodiutils.localize(30702, error=str(ex))) kodiutils.end_of_directory() return except GeoblockedException: kodiutils.ok_dialog(heading=kodiutils.localize(30709), message=kodiutils.localize( 30710)) # This video is geo-blocked... return except UnavailableException: kodiutils.ok_dialog(heading=kodiutils.localize(30711), message=kodiutils.localize( 30712)) # The video is unavailable... return # Play this item kodiutils.play(resolved_stream)
class TestEpg(unittest.TestCase): def __init__(self, *args, **kwargs): super(TestEpg, self).__init__(*args, **kwargs) self._auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password'), kodiutils.get_tokens_path()) def test_vier_today(self): epg = EpgApi() programs = epg.get_epg('vier', date.today().strftime('%Y-%m-%d')) self.assertIsInstance(programs, list) self.assertIsInstance(programs[0], EpgProgram) def test_vijf_today(self): epg = EpgApi() programs = epg.get_epg('vijf', date.today().strftime('%Y-%m-%d')) self.assertIsInstance(programs, list) self.assertIsInstance(programs[0], EpgProgram) def test_zes_today(self): epg = EpgApi() programs = epg.get_epg('zes', date.today().strftime('%Y-%m-%d')) self.assertIsInstance(programs, list) self.assertIsInstance(programs[0], EpgProgram) def test_unknown_today(self): epg = EpgApi() with self.assertRaises(Exception): epg.get_epg('vtm', date.today().strftime('%Y-%m-%d')) def test_vier_out_of_range(self): epg = EpgApi() programs = epg.get_epg('vier', '2020-01-01') self.assertEqual(programs, []) def test_play_video_from_epg(self): epg = EpgApi() epg_programs = epg.get_epg('vier', date.today().strftime('%Y-%m-%d')) epg_program = [program for program in epg_programs if program.video_url][0] # Lookup the Episode data since we don't have an UUID api = ContentApi(self._auth.get_token()) episode = api.get_episode(epg_program.channel, epg_program.video_url) self.assertIsInstance(episode, Episode) # Get stream based on the Episode's UUID video = api.get_stream(episode.channel, episode.uuid) self.assertTrue(video)
class Player: """ Code responsible for playing media """ def __init__(self): """ Initialise object """ self._auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password'), kodiutils.get_tokens_path()) self._api = ContentApi(self._auth.get_token()) def play_from_page(self, channel, path): """ Play the requested item. :type channel: string :type path: string """ # Get episode information episode = self._api.get_episode(channel, path) # Play this now we have the uuid self.play(channel, episode.uuid) def play(self, channel, item): """ Play the requested item. :type channel: string :type item: string """ try: # Get stream information resolved_stream = self._api.get_stream(channel, item) except GeoblockedException: kodiutils.ok_dialog(heading=kodiutils.localize(30709), message=kodiutils.localize(30710)) # This video is geo-blocked... return except UnavailableException: kodiutils.ok_dialog(heading=kodiutils.localize(30711), message=kodiutils.localize(30712)) # The video is unavailable... return # Play this item kodiutils.play(resolved_stream)
class BackgroundService(Monitor): """ Background service code """ def __init__(self): Monitor.__init__(self) self.update_interval = 24 * 3600 # Every 24 hours self.cache_expiry = 30 * 24 * 3600 # One month self._auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password'), kodiutils.get_tokens_path()) def run(self): """ Background loop for maintenance tasks """ _LOGGER.info('Service started') while not self.abortRequested(): # Update every `update_interval` after the last update if kodiutils.get_setting_bool('metadata_update') and int( kodiutils.get_setting('metadata_last_updated', 0)) + self.update_interval < time(): self._update_metadata() # Stop when abort requested if self.waitForAbort(10): break _LOGGER.info('Service stopped') def onSettingsChanged(self): # pylint: disable=invalid-name """ Callback when a setting has changed """ if self._has_credentials_changed(): _LOGGER.info('Clearing auth tokens due to changed credentials') self._auth.clear_tokens() # Refresh container kodiutils.container_refresh() @staticmethod def _has_credentials_changed(): """ Check if credentials have changed """ old_hash = kodiutils.get_setting('credentials_hash') new_hash = '' if kodiutils.get_setting('username') or kodiutils.get_setting( 'password'): new_hash = hashlib.md5((kodiutils.get_setting('username') + kodiutils.get_setting('password') ).encode('utf-8')).hexdigest() if new_hash != old_hash: kodiutils.set_setting('credentials_hash', new_hash) return True return False def _update_metadata(self): """ Update the metadata for the listings """ from resources.lib.modules.metadata import Metadata def update_status(_i, _total): """ Allow to cancel the background job """ return self.abortRequested( ) or not kodiutils.get_setting_bool('metadata_update') # Clear metadata that has expired for 30 days self._remove_expired_metadata(30 * 24 * 60 * 60) # Fetch new metadata success = Metadata().fetch_metadata(callback=update_status) # Update metadata_last_updated if success: kodiutils.set_setting('metadata_last_updated', str(int(time()))) @staticmethod def _remove_expired_metadata(keep_expired=None): """ Clear the cache """ path = kodiutils.get_cache_path() if not os.path.exists(path): return now = time() for filename in os.listdir(path): fullpath = path + filename if keep_expired and os.stat(fullpath).st_mtime + keep_expired > now: continue os.unlink(fullpath)
class Catalog: """ Menu code related to the catalog """ def __init__(self): """ Initialise object """ self._auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password'), kodiutils.get_tokens_path()) self._api = ContentApi(self._auth, cache_path=kodiutils.get_cache_path()) def show_catalog(self): """ Show all the programs of all channels """ try: items = self._api.get_programs() except Exception as ex: kodiutils.notification(message=str(ex)) raise listing = [Menu.generate_titleitem(item) for item in items] # Sort items by title # Used for A-Z listing or when movies and episodes are mixed. kodiutils.show_listing(listing, 30003, content='tvshows', sort='title') def show_catalog_channel(self, channel): """ Show the programs of a specific channel :type channel: str """ try: items = self._api.get_programs(channel) except Exception as ex: kodiutils.notification(message=str(ex)) raise listing = [] for item in items: listing.append(Menu.generate_titleitem(item)) # Sort items by title # Used for A-Z listing or when movies and episodes are mixed. kodiutils.show_listing(listing, 30003, content='tvshows', sort='title') def show_program(self, program_id): """ Show a program from the catalog :type program_id: str """ try: program = self._api.get_program( program_id, extract_clips=True, cache=CACHE_PREVENT ) # Use CACHE_PREVENT since we want fresh data except UnavailableException: kodiutils.ok_dialog(message=kodiutils.localize( 30717)) # This program is not available in the catalogue. kodiutils.end_of_directory() return if not program.episodes and not program.clips: kodiutils.ok_dialog(message=kodiutils.localize( 30717)) # This program is not available in the catalogue. kodiutils.end_of_directory() return # Go directly to the season when we have only one season and no clips if not program.clips and len(program.seasons) == 1: self.show_program_season(program_id, list(program.seasons.values())[0].uuid) return listing = [] # Add an '* All seasons' entry when configured in Kodi if program.seasons and kodiutils.get_global_setting( 'videolibrary.showallitems') is True: listing.append( TitleItem( title='* %s' % kodiutils.localize(30204), # * All seasons path=kodiutils.url_for('show_catalog_program_season', program=program_id, season='-1'), art_dict={ 'fanart': program.fanart, 'poster': program.poster, 'landscape': program.thumb, }, info_dict={ 'tvshowtitle': program.title, 'title': kodiutils.localize(30204), # All seasons 'plot': program.description, 'set': program.title, })) # Add the seasons for season in list(program.seasons.values()): listing.append( TitleItem( title=season. title, # kodiutils.localize(30205, season=season.number), # Season {season} path=kodiutils.url_for('show_catalog_program_season', program=program_id, season=season.uuid), art_dict={ 'fanart': program.fanart, 'poster': program.poster, 'landscape': program.thumb, }, info_dict={ 'tvshowtitle': program.title, 'title': kodiutils.localize( 30205, season=season.number), # Season {season} 'plot': season.description or program.description, 'set': program.title, })) # Add Clips if program.clips: listing.append( TitleItem( title=kodiutils.localize( 30059, program=program.title), # Clips for {program} path=kodiutils.url_for('show_catalog_program_clips', program=program_id), art_dict={ 'fanart': program.fanart, 'poster': program.poster, 'landscape': program.thumb, }, info_dict={ 'tvshowtitle': program.title, 'title': kodiutils.localize( 30059, program=program.title), # Clips for {program} 'plot': kodiutils.localize(30060, program=program.title ), # Watch short clips of {program} 'set': program.title, })) # Sort by label. Some programs return seasons unordered. kodiutils.show_listing(listing, 30003, content='tvshows') def show_program_season(self, program_id, season_uuid): """ Show the episodes of a program from the catalog :type program_id: str :type season_uuid: str """ try: program = self._api.get_program(program_id) except UnavailableException: kodiutils.ok_dialog(message=kodiutils.localize( 30717)) # This program is not available in the catalogue. kodiutils.end_of_directory() return if season_uuid == "-1": # Show all episodes episodes = program.episodes else: # Show the episodes of the season that was selected episodes = [ e for e in program.episodes if e.season_uuid == season_uuid ] listing = [Menu.generate_titleitem(episode) for episode in episodes] # Sort by episode number by default. Takes seasons into account. kodiutils.show_listing(listing, 30003, content='episodes', sort=['episode', 'duration']) def show_program_clips(self, program_id): """ Show the clips of a program from the catalog :type program_id: str """ try: # We need to query the backend, since we don't cache clips. program = self._api.get_program(program_id, extract_clips=True, cache=CACHE_PREVENT) except UnavailableException: kodiutils.ok_dialog(message=kodiutils.localize( 30717)) # This program is not available in the catalogue. kodiutils.end_of_directory() return listing = [ Menu.generate_titleitem(episode) for episode in program.clips ] # Sort like we get our results back. kodiutils.show_listing(listing, 30003, content='episodes') def show_categories(self): """ Shows the categories """ categories = self._api.get_categories() listing = [] for category in categories: listing.append( TitleItem(title=category.title, path=kodiutils.url_for('show_category', category=category.uuid), info_dict={ 'title': category.title, })) kodiutils.show_listing(listing, 30003, sort=['title']) def show_category(self, uuid): """ Shows a category """ programs = self._api.get_category_content(int(uuid)) listing = [Menu.generate_titleitem(program) for program in programs] kodiutils.show_listing(listing, 30003, content='tvshows') def show_recommendations(self): """ Shows the recommendations """ # "Meest bekeken" has a specific API endpoint, the other categories are scraped from the website. listing = [ TitleItem(title='Meest bekeken', path=kodiutils.url_for('show_recommendations_category', category='meest-bekeken'), info_dict={ 'title': 'Meest bekeken', }) ] recommendations = self._api.get_recommendation_categories() for category in recommendations: listing.append( TitleItem(title=category.title, path=kodiutils.url_for( 'show_recommendations_category', category=category.uuid), info_dict={ 'title': category.title, })) kodiutils.show_listing(listing, 30005, content='tvshows') def show_recommendations_category(self, uuid): """ Shows the a category of the recommendations """ if uuid == 'meest-bekeken': programs = self._api.get_popular_programs() episodes = [] else: recommendations = self._api.get_recommendation_categories() category = next(category for category in recommendations if category.uuid == uuid) programs = category.programs episodes = category.episodes listing = [] for episode in episodes: title_item = Menu.generate_titleitem(episode) title_item.info_dict[ 'title'] = episode.program_title + ' - ' + title_item.title listing.append(title_item) for program in programs: listing.append(Menu.generate_titleitem(program)) kodiutils.show_listing(listing, 30005, content='tvshows') def show_mylist(self): """ Show all the programs of all channels """ try: mylist, _ = self._auth.get_dataset('myList', 'myList') except Exception as ex: kodiutils.notification(message=str(ex)) raise items = [] if mylist: for item in mylist: program = self._api.get_program_by_uuid(item.get('id')) if program: program.my_list = True items.append(program) listing = [Menu.generate_titleitem(item) for item in items] # Sort items by title # Used for A-Z listing or when movies and episodes are mixed. kodiutils.show_listing(listing, 30011, content='tvshows', sort='title') def mylist_add(self, uuid): """ Add a program to My List """ if not uuid: kodiutils.end_of_directory() return mylist, sync_info = self._auth.get_dataset('myList', 'myList') if not mylist: mylist = [] if uuid not in [item.get('id') for item in mylist]: # Python 2.7 doesn't support .timestamp(), and windows doesn't do '%s', so we need to calculate it ourself epoch = datetime(1970, 1, 1, tzinfo=dateutil.tz.gettz('UTC')) now = datetime.now(tz=dateutil.tz.gettz('UTC')) timestamp = int((now - epoch).total_seconds()) * 1000 mylist.append({ 'id': uuid, 'timestamp': timestamp, }) self._auth.put_dataset('myList', 'myList', mylist, sync_info) kodiutils.end_of_directory() def mylist_del(self, uuid): """ Remove a program from My List """ if not uuid: kodiutils.end_of_directory() return mylist, sync_info = self._auth.get_dataset('myList', 'myList') if not mylist: mylist = [] new_mylist = [item for item in mylist if item.get('id') != uuid] self._auth.put_dataset('myList', 'myList', new_mylist, sync_info) kodiutils.end_of_directory()
def __init__(self, *args, **kwargs): super(TestAuth, self).__init__(*args, **kwargs) self._auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password'), kodiutils.get_tokens_path())
class Catalog: """ Menu code related to the catalog """ def __init__(self): """ Initialise object """ self._auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password'), kodiutils.get_tokens_path()) self._api = ContentApi(self._auth.get_token()) self._menu = Menu() def show_catalog(self): """ Show all the programs of all channels """ try: items = [] for channel in list(CHANNELS): items.extend(self._api.get_programs(channel)) except Exception as ex: kodiutils.notification(message=str(ex)) raise listing = [self._menu.generate_titleitem(item) for item in items] # Sort items by label, but don't put folders at the top. # Used for A-Z listing or when movies and episodes are mixed. kodiutils.show_listing(listing, 30003, content='tvshows', sort='label') def show_catalog_channel(self, channel): """ Show the programs of a specific channel :type channel: str """ try: items = self._api.get_programs(channel) except Exception as ex: kodiutils.notification(message=str(ex)) raise listing = [] for item in items: listing.append(self._menu.generate_titleitem(item)) # Sort items by label, but don't put folders at the top. # Used for A-Z listing or when movies and episodes are mixed. kodiutils.show_listing(listing, 30003, content='tvshows', sort='label') def show_program(self, channel, program_id): """ Show a program from the catalog :type channel: str :type program_id: str """ try: program = self._api.get_program(channel, program_id) except UnavailableException: kodiutils.ok_dialog( message=kodiutils.localize(30717) ) # This program is not available in the Vier/Vijf/Zes catalogue. kodiutils.end_of_directory() return if not program.episodes: kodiutils.ok_dialog( message=kodiutils.localize(30717) ) # This program is not available in the Vier/Vijf/Zes catalogue. kodiutils.end_of_directory() return # Go directly to the season when we have only one season if len(program.seasons) == 1: self.show_program_season(channel, program_id, program.seasons.values()[0].number) return studio = CHANNELS.get(program.channel, {}).get('studio_icon') listing = [] # Add an '* All seasons' entry when configured in Kodi if kodiutils.get_global_setting('videolibrary.showallitems') is True: listing.append( TitleItem( title='* %s' % kodiutils.localize(30204), # * All seasons path=kodiutils.url_for('show_catalog_program_season', channel=channel, program=program_id, season=-1), art_dict={ 'thumb': program.cover, 'fanart': program.background, }, info_dict={ 'tvshowtitle': program.title, 'title': kodiutils.localize(30204), # All seasons 'plot': program.description, 'set': program.title, 'studio': studio, })) # Add the seasons for s in list(program.seasons.values()): listing.append( TitleItem( title=s. title, # kodiutils.localize(30205, season=s.number), # Season {season} path=kodiutils.url_for('show_catalog_program_season', channel=channel, program=program_id, season=s.number), art_dict={ 'thumb': s.cover, 'fanart': program.background, }, info_dict={ 'tvshowtitle': program.title, 'title': kodiutils.localize(30205, season=s.number), # Season {season} 'plot': s.description, 'set': program.title, 'studio': studio, })) # Sort by label. Some programs return seasons unordered. kodiutils.show_listing(listing, 30003, content='tvshows', sort=['label']) def show_program_season(self, channel, program_id, season): """ Show the episodes of a program from the catalog :type channel: str :type program_id: str :type season: int """ try: program = self._api.get_program(channel, program_id) except UnavailableException: kodiutils.ok_dialog( message=kodiutils.localize(30717) ) # This program is not available in the Vier/Vijf/Zes catalogue. kodiutils.end_of_directory() return if season == -1: # Show all episodes episodes = program.episodes else: # Show the episodes of the season that was selected episodes = [e for e in program.episodes if e.season == season] listing = [ self._menu.generate_titleitem(episode) for episode in episodes ] # Sort by episode number by default. Takes seasons into account. kodiutils.show_listing(listing, 30003, content='episodes', sort=['episode', 'duration'])