class Metadata: """ Code responsible for the refreshing of the metadata """ def __init__(self, kodi): """ Initialise object :type kodi: resources.lib.kodiwrapper.KodiWrapper """ self._kodi = kodi self._vtm_go = VtmGo(self._kodi) def update(self): """ Update the metadata with a foreground progress indicator """ # Create progress indicator progress = self._kodi.show_progress( message=self._kodi.localize(30715)) # Updating metadata def update_status(i, total): """ Update the progress indicator """ progress.update( int(((i + 1) / total) * 100), self._kodi.localize( 30716, index=i + 1, total=total)) # Updating metadata ({index}/{total}) return progress.iscanceled() self.fetch_metadata(callback=update_status) # Close progress indicator progress.close() def fetch_metadata(self, callback=None): """ Fetch the metadata for all the items in the catalog :type callback: callable """ # Fetch all items from the catalog items = self._vtm_go.get_items() count = len(items) # Loop over all of them and download the metadata for index, item in enumerate(items): if isinstance(item, Movie): if not self._vtm_go.get_movie(item.movie_id, cache=True): self._vtm_go.get_movie(item.movie_id) elif isinstance(item, Program): if not self._vtm_go.get_program(item.program_id, cache=True): self._vtm_go.get_program(item.program_id) # Update the progress indicator if callback and callback(index, count): # Stop when callback returns False return False return True def clean(self): """ Clear metadata (called from settings) """ self._kodi.invalidate_cache() self._kodi.set_setting('metadata_last_updated', '0') self._kodi.show_ok_dialog( message=self._kodi.localize(30714)) # Local metadata is cleared
class Metadata: """ Code responsible for the refreshing of the metadata """ def __init__(self): """ Initialise object """ self._auth = VtmGoAuth(kodiutils.get_setting('username'), kodiutils.get_setting('password'), 'VTM', kodiutils.get_setting('profile'), kodiutils.get_tokens_path()) self._vtm_go = VtmGo(self._auth) def update(self): """ Update the metadata with a foreground progress indicator """ # Create progress indicator progress_dialog = kodiutils.progress(message=kodiutils.localize(30715)) # Updating metadata def update_status(i, total): """ Update the progress indicator """ progress_dialog.update(int(((i + 1) / total) * 100), kodiutils.localize(30716, index=i + 1, total=total)) # Updating metadata ({index}/{total}) return progress_dialog.iscanceled() self.fetch_metadata(callback=update_status) # Close progress indicator progress_dialog.close() def fetch_metadata(self, callback=None): """ Fetch the metadata for all the items in the catalog :type callback: callable """ # Fetch a list of all items from the catalog items = self._vtm_go.get_items() count = len(items) # Loop over all of them and download the metadata for index, item in enumerate(items): if isinstance(item, Movie): self._vtm_go.get_movie(item.movie_id) elif isinstance(item, Program): self._vtm_go.get_program(item.program_id) # Run callback after every item if callback and callback(index, count): # Stop when callback returns False return False return True @staticmethod def clean(): """ Clear metadata (called from settings) """ kodiutils.invalidate_cache() kodiutils.set_setting('metadata_last_updated', '0') kodiutils.ok_dialog(message=kodiutils.localize(30714)) # Local metadata is cleared
class Library: """ Menu code related to the catalog """ def __init__(self): """ Initialise object """ self._auth = VtmGoAuth(kodiutils.get_setting('username'), kodiutils.get_setting('password'), 'VTM', kodiutils.get_setting('profile'), kodiutils.get_tokens_path()) self._api = VtmGo(self._auth) def show_library_movies(self, movie=None): """ Return a list of the movies that should be exported. """ if movie is None: if kodiutils.get_setting_int( 'library_movies') == LIBRARY_FULL_CATALOG: # Full catalog # Use cache if available, fetch from api otherwise so we get rich metadata for new content items = self._api.get_items(content_filter=Movie, cache=CACHE_AUTO) else: # Only favourites, use cache if available, fetch from api otherwise items = self._api.get_mylist(content_filter=Movie) else: items = [self._api.get_movie(movie)] listing = [] for item in items: title_item = Menu.generate_titleitem(item) # title_item.path = kodiutils.url_for('library_movies', movie=item.movie_id) # We need a trailing / title_item.path = 'plugin://plugin.video.vtm.go/library/movies/?movie=%s' % item.movie_id listing.append(title_item) kodiutils.show_listing(listing, 30003, content='movies', sort=['label', 'year', 'duration']) def show_library_tvshows(self, program=None): """ Return a list of the series that should be exported. """ if program is None: if kodiutils.get_setting_int( 'library_tvshows') == LIBRARY_FULL_CATALOG: # Full catalog # Use cache if available, fetch from api otherwise so we get rich metadata for new content # NOTE: We should probably use CACHE_PREVENT here, so we can pick up new episodes, but we can't since that would # require a massive amount of API calls for each update. We do this only for programs in 'My list'. items = self._api.get_items(content_filter=Program, cache=CACHE_AUTO) else: # Only favourites, don't use cache, fetch from api # If we use CACHE_AUTO, we will miss updates until the user manually opens the program in the Add-on items = self._api.get_mylist(content_filter=Program, cache=CACHE_PREVENT) else: # Fetch only a single program items = [self._api.get_program(program, cache=CACHE_PREVENT)] listing = [] for item in items: title_item = Menu.generate_titleitem(item) # title_item.path = kodiutils.url_for('library_tvshows', program=item.program_id) # We need a trailing / title_item.path = 'plugin://plugin.video.vtm.go/library/tvshows/?program={program_id}'.format( program_id=item.program_id) listing.append(title_item) kodiutils.show_listing(listing, 30003, content='tvshows', sort=['label', 'year', 'duration']) def show_library_tvshows_program(self, program): """ Return a list of the episodes that should be exported. """ program_obj = self._api.get_program(program) listing = [] for season in list(program_obj.seasons.values()): for item in list(season.episodes.values()): title_item = Menu.generate_titleitem(item) # title_item.path = kodiutils.url_for('library_tvshows', program=item.program_id, episode=item.episode_id) title_item.path = 'plugin://plugin.video.vtm.go/library/tvshows/?program={program_id}&episode={episode_id}'.format( program_id=item.program_id, episode_id=item.episode_id) listing.append(title_item) # Sort by episode number by default. Takes seasons into account. kodiutils.show_listing(listing, 30003, content='episodes', sort=['episode', 'duration']) def check_library_movie(self, movie): """ Check if the given movie is still available. """ _LOGGER.debug('Checking if movie %s is still available', movie) # Our parent path always exists if movie is None: kodiutils.library_return_status(True) return if kodiutils.get_setting_int('library_movies') == LIBRARY_FULL_CATALOG: id_list = self._api.get_catalog_ids() else: id_list = self._api.get_mylist_ids() kodiutils.library_return_status(movie in id_list) def check_library_tvshow(self, program): """ Check if the given program is still available. """ _LOGGER.debug('Checking if program %s is still available', program) # Our parent path always exists if program is None: kodiutils.library_return_status(True) return if kodiutils.get_setting_int( 'library_tvshows') == LIBRARY_FULL_CATALOG: id_list = self._api.get_catalog_ids() else: id_list = self._api.get_mylist_ids() kodiutils.library_return_status(program in id_list) @staticmethod def mylist_added(video_type, content_id): """ Something has been added to My List. We want to index this. """ if video_type == CONTENT_TYPE_MOVIE: if kodiutils.get_setting_int( 'library_movies') != LIBRARY_ONLY_MYLIST: return # This unfortunately adds the movie to the database with the wrong parent path: # Library().update('plugin://plugin.video.vtm.go/library/movies/?movie=%s&kodi_action=refresh_info' % content_id) Library().update('plugin://plugin.video.vtm.go/library/movies/') elif video_type == CONTENT_TYPE_PROGRAM: if kodiutils.get_setting_int( 'library_tvshows') != LIBRARY_ONLY_MYLIST: return Library().update( 'plugin://plugin.video.vtm.go/library/tvshows/?program=%s&kodi_action=refresh_info' % content_id) @staticmethod def mylist_removed(video_type, content_id): """ Something has been removed from My List. We want to de-index this. """ if video_type == CONTENT_TYPE_MOVIE: if kodiutils.get_setting_int( 'library_movies') != LIBRARY_ONLY_MYLIST: return Library().clean( 'plugin://plugin.video.vtm.go/library/movies/?movie=%s' % content_id) elif video_type == CONTENT_TYPE_PROGRAM: if kodiutils.get_setting_int( 'library_tvshows') != LIBRARY_ONLY_MYLIST: return Library().clean( 'plugin://plugin.video.vtm.go/library/tvshows/?program=%s' % content_id) @staticmethod def configure(): """ Configure the library integration. """ # There seems to be no way to add sources automatically. # * https://forum.kodi.tv/showthread.php?tid=228840 # Open the sources view kodiutils.execute_builtin('ActivateWindow(Videos,sources://video/)') @staticmethod def update(path=None): """ Update the library integration. """ _LOGGER.debug('Scanning %s', path) if path: # We can use this to instantly add something to the library when we've added it to 'My List'. kodiutils.jsonrpc(method='VideoLibrary.Scan', params=dict( directory=path, showdialogs=False, )) else: kodiutils.jsonrpc(method='VideoLibrary.Scan') @staticmethod def clean(path=None): """ Cleanup the library integration. """ _LOGGER.debug('Cleaning %s', path) if path: # We can use this to instantly remove something from the library when we've removed it from 'My List'. # This only works from Kodi 19 however. See https://github.com/xbmc/xbmc/pull/18562 if kodiutils.kodi_version_major() > 18: kodiutils.jsonrpc(method='VideoLibrary.Clean', params=dict( directory=path, showdialogs=False, )) else: kodiutils.jsonrpc(method='VideoLibrary.Clean', params=dict(showdialogs=False, )) else: kodiutils.jsonrpc(method='VideoLibrary.Clean')
class Player: """ Code responsible for playing media """ def __init__(self): """ Initialise object """ try: self._auth = VtmGoAuth(kodiutils.get_setting('username'), kodiutils.get_setting('password'), 'VTM', kodiutils.get_setting('profile'), kodiutils.get_tokens_path()) except NoLoginException: self._auth = None self._vtm_go = VtmGo(self._auth) self._vtm_go_stream = VtmGoStream(self._auth) def play_or_live(self, category, item, channel): """ Ask to play the requested item or switch to the live channel :type category: str :type item: str :type channel: str """ res = kodiutils.show_context_menu([kodiutils.localize(30103), kodiutils.localize(30105)]) # Watch Live | Play from Catalog if res == -1: # user has cancelled return if res == 0: # user selected "Watch Live" # Play live self.play('channels', channel) return # Play this program self.play(category, item) def play(self, category, item): """ Play the requested item. :type category: string :type item: string """ if not self._check_credentials(): kodiutils.end_of_directory() return # Check if inputstreamhelper is correctly installed if not self._check_inputstream(): kodiutils.end_of_directory() return try: # Get stream information resolved_stream = self._vtm_go_stream.get_stream(category, item) except StreamGeoblockedException: kodiutils.ok_dialog(heading=kodiutils.localize(30709), message=kodiutils.localize(30710)) # This video is geo-blocked... kodiutils.end_of_directory() return except StreamUnavailableException: kodiutils.ok_dialog(heading=kodiutils.localize(30711), message=kodiutils.localize(30712)) # The video is unavailable... kodiutils.end_of_directory() return info_dict = { 'tvshowtitle': resolved_stream.program, 'title': resolved_stream.title, 'duration': resolved_stream.duration, } prop_dict = {} stream_dict = { 'duration': resolved_stream.duration, } upnext_data = None # Lookup metadata try: if category in ['movies', 'oneoffs']: info_dict.update({'mediatype': 'movie'}) # Get details movie_details = self._vtm_go.get_movie(item) if movie_details: info_dict.update({ 'plot': movie_details.description, 'year': movie_details.year, 'aired': movie_details.aired, }) elif category == 'episodes': info_dict.update({'mediatype': 'episode'}) # There is no direct API to get episode details, so we go trough the cached program details program = self._vtm_go.get_program(resolved_stream.program_id) if program: episode_details = self._vtm_go.get_episode_from_program(program, item) if episode_details: info_dict.update({ 'plot': episode_details.description, 'season': episode_details.season, 'episode': episode_details.number, }) # Lookup the next episode next_episode_details = self._vtm_go.get_next_episode_from_program(program, episode_details.season, episode_details.number) # Create the data for Up Next if next_episode_details: upnext_data = self.generate_upnext(episode_details, next_episode_details) elif category == 'channels': info_dict.update({'mediatype': 'episode'}) # For live channels, we need to keep on updating the manifest # This might not be needed, and could be done with the Location-tag updates if inputstream.adaptive supports it # See https://github.com/peak3d/inputstream.adaptive/pull/298#issuecomment-524206935 prop_dict.update({ 'inputstream.adaptive.manifest_update_parameter': 'full', }) else: _LOGGER.warning('Unknown category %s', category) except UnavailableException: # We continue without details. # This allows to play some programs that don't have metadata (yet). pass # If we have enabled the Manifest proxy, route the call trough that. if category in ['movies', 'oneoffs', 'episodes'] and kodiutils.get_setting_bool('manifest_proxy'): try: # Python 3 from urllib.parse import urlencode except ImportError: # Python 2 from urllib import urlencode port = kodiutils.get_setting_int('manifest_proxy_port') if not port: kodiutils.notification(message=kodiutils.localize(30718), icon='error') kodiutils.end_of_directory() return url = 'http://127.0.0.1:{port}/manifest?{path}'.format(port=port, path=urlencode({'path': resolved_stream.url})) else: url = resolved_stream.url license_key = self._vtm_go_stream.create_license_key(resolved_stream.license_url) # Play this item kodiutils.play(url, license_key, resolved_stream.title, {}, info_dict, prop_dict, stream_dict) # Wait for playback to start kodi_player = KodiPlayer() if not kodi_player.waitForPlayBack(url=url): # Playback didn't start kodiutils.end_of_directory() return # Send Up Next data if upnext_data and kodiutils.get_setting_bool('useupnext'): _LOGGER.debug("Sending Up Next data: %s", upnext_data) self.send_upnext(upnext_data) @staticmethod def _check_credentials(): """ Check if the user has credentials """ if kodiutils.has_credentials(): return True # You need to configure your credentials before you can access the content of VTM GO. confirm = kodiutils.yesno_dialog(message=kodiutils.localize(30701)) if confirm: kodiutils.open_settings() if kodiutils.has_credentials(): return True return False @staticmethod def _check_inputstream(): """ Check if inputstreamhelper and inputstream.adaptive are fine. :rtype boolean """ try: from inputstreamhelper import Helper is_helper = Helper('mpd', drm='com.widevine.alpha') if not is_helper.check_inputstream(): # inputstreamhelper has already shown an error return False except ImportError: kodiutils.ok_dialog(message=kodiutils.localize(30708)) # Please reboot Kodi return False return True @staticmethod def generate_upnext(current_episode, next_episode): """ Construct the data for Up Next. :type current_episode: resources.lib.vtmgo.vtmgo.Episode :type next_episode: resources.lib.vtmgo.vtmgo.Episode """ upnext_info = dict( current_episode=dict( episodeid=current_episode.episode_id, tvshowid=current_episode.program_id, title=current_episode.name, art={ 'poster': current_episode.poster, 'landscape': current_episode.thumb, 'fanart': current_episode.fanart, }, season=current_episode.season, episode=current_episode.number, showtitle=current_episode.program_name, plot=current_episode.description, playcount=None, rating=None, firstaired=current_episode.aired[:10] if current_episode.aired else '', runtime=current_episode.duration, ), next_episode=dict( episodeid=next_episode.episode_id, tvshowid=next_episode.program_id, title=next_episode.name, art={ 'poster': next_episode.poster, 'landscape': next_episode.thumb, 'fanart': next_episode.fanart, }, season=next_episode.season, episode=next_episode.number, showtitle=next_episode.program_name, plot=next_episode.description, playcount=None, rating=None, firstaired=next_episode.aired[:10] if next_episode.aired else '', runtime=next_episode.duration, ), play_url='plugin://plugin.video.vtm.go/play/catalog/episodes/%s' % next_episode.episode_id, ) return upnext_info @staticmethod def send_upnext(upnext_info): """ Send a message to Up Next with information about the next Episode. :type upnext_info: object """ from base64 import b64encode from json import dumps data = [kodiutils.to_unicode(b64encode(dumps(upnext_info).encode()))] sender = '{addon_id}.SIGNAL'.format(addon_id='plugin.video.vtm.go') kodiutils.notify(sender=sender, message='upnext_data', data=data)
class Catalog: """ Menu code related to the catalog """ def __init__(self): """ Initialise object """ self._auth = VtmGoAuth(kodiutils.get_setting('username'), kodiutils.get_setting('password'), 'VTM', kodiutils.get_setting('profile'), kodiutils.get_tokens_path()) self._vtm_go = VtmGo(self._auth) self._menu = Menu() def show_catalog(self): """ Show the catalog """ try: categories = self._vtm_go.get_categories() except ApiUpdateRequired: kodiutils.ok_dialog(message=kodiutils.localize( 30705)) # The VTM GO Service has been updated... return except Exception as ex: # pylint: disable=broad-except _LOGGER.error("%s", ex) kodiutils.ok_dialog(message="%s" % ex) return listing = [] for cat in categories: listing.append( kodiutils.TitleItem( title=cat.title, path=kodiutils.url_for('show_catalog_category', category=cat.category_id), info_dict=dict( plot='[B]{category}[/B]'.format(category=cat.title), ), )) # Sort categories by default like in VTM GO. kodiutils.show_listing(listing, 30003, content='files') def show_catalog_category(self, category=None): """ Show a category in the catalog :type category: str """ try: items = self._vtm_go.get_items(category) except ApiUpdateRequired: kodiutils.ok_dialog(message=kodiutils.localize( 30705)) # The VTM GO Service has been updated... return except Exception as ex: # pylint: disable=broad-except _LOGGER.error("%s", ex) kodiutils.ok_dialog(message="%s" % ex) return 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='movies' if category == 'films' else 'tvshows', sort=['label', 'year', 'duration']) def show_catalog_channel(self, channel): """ Show a category in the catalog :type channel: str """ try: items = self._vtm_go.get_items() except ApiUpdateRequired: kodiutils.ok_dialog(message=kodiutils.localize( 30705)) # The VTM GO Service has been updated... return except Exception as ex: # pylint: disable=broad-except _LOGGER.error("%s", ex) kodiutils.ok_dialog(message="%s" % ex) return listing = [] for item in items: if item.channel == channel: 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, program): """ Show a program from the catalog :type program: str """ try: program_obj = self._vtm_go.get_program( program, 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 VTM GO catalogue. kodiutils.end_of_directory() return # Go directly to the season when we have only one season if len(program_obj.seasons) == 1: self.show_program_season( program, list(program_obj.seasons.values())[0].number) return studio = CHANNELS.get(program_obj.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( kodiutils.TitleItem( title='* %s' % kodiutils.localize(30204), # * All seasons path=kodiutils.url_for('show_catalog_program_season', program=program, season=-1), art_dict=dict( thumb=program_obj.cover, fanart=program_obj.cover, ), info_dict=dict( tvshowtitle=program_obj.name, title=kodiutils.localize(30204), # All seasons tagline=program_obj.description, set=program_obj.name, studio=studio, mpaa=', '.join(program_obj.legal) if hasattr(program_obj, 'legal') and program_obj.legal else kodiutils.localize(30216), # All ages ), )) # Add the seasons for season in list(program_obj.seasons.values()): listing.append( kodiutils.TitleItem( title=kodiutils.localize( 30205, season=season.number), # Season {season} path=kodiutils.url_for('show_catalog_program_season', program=program, season=season.number), art_dict=dict( thumb=season.cover, fanart=program_obj.cover, ), info_dict=dict( tvshowtitle=program_obj.name, title=kodiutils.localize( 30205, season=season.number), # Season {season} tagline=program_obj.description, set=program_obj.name, studio=studio, mpaa=', '.join(program_obj.legal) if hasattr(program_obj, 'legal') and program_obj.legal else kodiutils.localize(30216), # All ages ), )) # Sort by label. Some programs return seasons unordered. kodiutils.show_listing(listing, 30003, content='tvshows', sort=['label']) def show_program_season(self, program, season): """ Show the episodes of a program from the catalog :type program: str :type season: int """ try: program_obj = self._vtm_go.get_program( program ) # Use CACHE_AUTO since the data is just refreshed in show_program except UnavailableException: kodiutils.ok_dialog( message=kodiutils.localize(30717) ) # This program is not available in the VTM GO catalogue. kodiutils.end_of_directory() return if season == -1: # Show all seasons seasons = list(program_obj.seasons.values()) else: # Show the season that was selected seasons = [program_obj.seasons[season]] listing = [ self._menu.generate_titleitem(e) for s in seasons for e in list(s.episodes.values()) ] # Sort by episode number by default. Takes seasons into account. kodiutils.show_listing(listing, 30003, content='episodes', sort=['episode', 'duration']) def show_recommendations(self, storefront): """ Show the recommendations. :type storefront: str """ try: recommendations = self._vtm_go.get_recommendations(storefront) except ApiUpdateRequired: kodiutils.ok_dialog(message=kodiutils.localize( 30705)) # The VTM GO Service has been updated... return except Exception as ex: # pylint: disable=broad-except _LOGGER.error("%s", ex) kodiutils.ok_dialog(message="%s" % ex) return listing = [] for cat in recommendations: listing.append( kodiutils.TitleItem( title=cat.title, path=kodiutils.url_for('show_recommendations_category', storefront=storefront, category=cat.category_id), info_dict=dict( plot='[B]{category}[/B]'.format(category=cat.title), ), )) # Sort categories by default like in VTM GO. kodiutils.show_listing(listing, 30015, content='files') def show_recommendations_category(self, storefront, category): """ Show the items in a recommendations category. :type storefront: str :type category: str """ try: recommendations = self._vtm_go.get_recommendations(storefront) except ApiUpdateRequired: kodiutils.ok_dialog(message=kodiutils.localize( 30705)) # The VTM GO Service has been updated... return except Exception as ex: # pylint: disable=broad-except _LOGGER.error("%s", ex) kodiutils.ok_dialog(message="%s" % ex) return listing = [] for cat in recommendations: # Only show the requested category if cat.category_id != category: continue for item in cat.content: listing.append(self._menu.generate_titleitem(item)) # Sort categories by default like in VTM GO. kodiutils.show_listing(listing, 30015, content='tvshows') def show_mylist(self): """ Show the items in "My List" """ try: mylist = self._vtm_go.get_swimlane('my-list') except ApiUpdateRequired: kodiutils.ok_dialog(message=kodiutils.localize( 30705)) # The VTM GO Service has been updated... return except Exception as ex: # pylint: disable=broad-except _LOGGER.error("%s", ex) kodiutils.ok_dialog(message="%s" % ex) return listing = [] for item in mylist: item.my_list = True listing.append(self._menu.generate_titleitem(item)) # Sort categories by default like in VTM GO. kodiutils.show_listing(listing, 30017, content='tvshows') def mylist_add(self, video_type, content_id): """ Add an item to "My List" :type video_type: str :type content_id: str """ self._vtm_go.add_mylist(video_type, content_id) kodiutils.end_of_directory() def mylist_del(self, video_type, content_id): """ Remove an item from "My List" :type video_type: str :type content_id: str """ self._vtm_go.del_mylist(video_type, content_id) kodiutils.end_of_directory() kodiutils.container_refresh() def show_continuewatching(self): """ Show the items in "Continue Watching" """ try: mylist = self._vtm_go.get_swimlane('continue-watching') except ApiUpdateRequired: kodiutils.ok_dialog(message=kodiutils.localize( 30705)) # The VTM GO Service has been updated... return except Exception as ex: # pylint: disable=broad-except _LOGGER.error("%s", ex) kodiutils.ok_dialog(message="%s" % ex) return listing = [] for item in mylist: titleitem = self._menu.generate_titleitem(item, progress=True) # Add Program Name to title since this list contains episodes from multiple programs title = '%s - %s' % (titleitem.info_dict.get('tvshowtitle'), titleitem.info_dict.get('title')) titleitem.info_dict['title'] = title listing.append(titleitem) # Sort categories by default like in VTM GO. kodiutils.show_listing(listing, 30019, content='episodes', sort='label')
class Catalog: """ Menu code related to the catalog """ def __init__(self, kodi): """ Initialise object :type kodi: KodiWrapper """ self._kodi = kodi self._vtm_go = VtmGo(self._kodi) self._menu = Menu(self._kodi) def show_catalog(self): """ Show the catalog """ try: categories = self._vtm_go.get_categories() except Exception as ex: self._kodi.show_notification(message=str(ex)) raise listing = [] for cat in categories: listing.append( TitleItem(title=cat.title, path=self._kodi.url_for('show_catalog_category', kids=self._kodi.kids_mode(), category=cat.category_id), info_dict={ 'plot': '[B]{category}[/B]'.format(category=cat.title), })) # Sort categories by default like in VTM GO. self._kodi.show_listing(listing, 30003, content='files') def show_catalog_category(self, category=None): """ Show a category in the catalog :type category: str """ try: items = self._vtm_go.get_items(category) except Exception as ex: self._kodi.show_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. self._kodi.show_listing( listing, 30003, content='movies' if category == 'films' else 'tvshows', sort='label') def show_catalog_channel(self, channel): """ Show a category in the catalog :type channel: str """ try: items = self._vtm_go.get_items() except Exception as ex: self._kodi.show_notification(message=str(ex)) raise listing = [] for item in items: if item.channel == channel: 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. self._kodi.show_listing(listing, 30003, content='tvshows', sort='label') def show_program(self, program): """ Show a program from the catalog :type program: str """ try: program_obj = self._vtm_go.get_program(program) except UnavailableException: self._kodi.show_ok_dialog( message=self._kodi.localize(30717) ) # This program is not available in the VTM GO catalogue. self._kodi.end_of_directory() return studio = CHANNELS.get(program_obj.channel, {}).get('studio_icon') listing = [] # Add an '* All seasons' entry when configured in Kodi if self._kodi.get_global_setting('videolibrary.showallitems') is True: listing.append( TitleItem( title='* %s' % self._kodi.localize(30204), # * All seasons path=self._kodi.url_for('show_catalog_program_season', program=program, season=-1), art_dict={ 'thumb': program_obj.cover, 'fanart': program_obj.cover, }, info_dict={ 'tvshowtitle': program_obj.name, 'title': self._kodi.localize(30204), # All seasons 'tagline': program_obj.description, 'set': program_obj.name, 'studio': studio, 'mpaa': ', '.join(program_obj.legal) if hasattr(program_obj, 'legal') and program_obj.legal else self._kodi.localize(30216), })) # Add the seasons for s in program_obj.seasons.values(): listing.append( TitleItem( title=self._kodi.localize(30205, season=s.number), # Season X path=self._kodi.url_for('show_catalog_program_season', program=program, season=s.number), art_dict={ 'thumb': s.cover, 'fanart': program_obj.cover, }, info_dict={ 'tvshowtitle': program_obj.name, 'title': self._kodi.localize(30205, season=s.number), 'tagline': program_obj.description, 'set': program_obj.name, 'studio': studio, 'mpaa': ', '.join(program_obj.legal) if hasattr(program_obj, 'legal') and program_obj.legal else self._kodi.localize(30216), })) # Sort by label. Some programs return seasons unordered. self._kodi.show_listing(listing, 30003, content='tvshows', sort='label') def show_program_season(self, program, season): """ Show a program from the catalog :type program: str :type season: int """ try: program_obj = self._vtm_go.get_program(program) except UnavailableException: self._kodi.show_ok_dialog( message=self._kodi.localize(30717) ) # This program is not available in the VTM GO catalogue. self._kodi.end_of_directory() return if season == -1: # Show all seasons seasons = program_obj.seasons.values() else: # Show the season that was selected seasons = [program_obj.seasons[season]] listing = [] for s in seasons: for episode in s.episodes.values(): listing.append(self._menu.generate_titleitem(episode)) # Sort by episode number by default. Takes seasons into account. self._kodi.show_listing(listing, 30003, content='episodes', sort='episode') def show_recommendations(self): """ Show the recommendations """ try: recommendations = self._vtm_go.get_recommendations() except Exception as ex: self._kodi.show_notification(message=str(ex)) raise listing = [] for cat in recommendations: listing.append( TitleItem(title=cat.title, path=self._kodi.url_for( 'show_recommendations_category', kids=self._kodi.kids_mode(), category=cat.category_id), info_dict={ 'plot': '[B]{category}[/B]'.format(category=cat.title), })) # Sort categories by default like in VTM GO. self._kodi.show_listing(listing, 30015, content='files') def show_recommendations_category(self, category): """ Show the items in a recommendations category :type category: str """ try: recommendations = self._vtm_go.get_recommendations() except Exception as ex: self._kodi.show_notification(message=str(ex)) raise listing = [] for cat in recommendations: # Only show the requested category if cat.category_id != category: continue for item in cat.content: listing.append(self._menu.generate_titleitem(item)) # Sort categories by default like in VTM GO. self._kodi.show_listing(listing, 30015, content='tvshows') def show_mylist(self): """ Show the items in "My List" """ try: mylist = self._vtm_go.get_swimlane('my-list') except Exception as ex: self._kodi.show_notification(message=str(ex)) raise listing = [] for item in mylist: item.my_list = True listing.append(self._menu.generate_titleitem(item)) # Sort categories by default like in VTM GO. self._kodi.show_listing(listing, 30017, content='tvshows') def mylist_add(self, video_type, content_id): """ Add an item to "My List" :type video_type: str :type content_id: str """ self._vtm_go.add_mylist(video_type, content_id) self._kodi.end_of_directory() def mylist_del(self, video_type, content_id): """ Remove an item from "My List" :type video_type: str :type content_id: str """ self._vtm_go.del_mylist(video_type, content_id) self._kodi.end_of_directory() self._kodi.container_refresh() def show_continuewatching(self): """ Show the items in "Continue Watching" """ try: mylist = self._vtm_go.get_swimlane('continue-watching') except Exception as ex: self._kodi.show_notification(message=str(ex)) raise listing = [] for item in mylist: titleitem = self._menu.generate_titleitem(item, progress=True) # Add Program Name to title since this list contains episodes from multiple programs title = '%s - %s' % (titleitem.info_dict.get('tvshowtitle'), titleitem.info_dict.get('title')) titleitem.title = title titleitem.info_dict['title'] = title listing.append(titleitem) # Sort categories by default like in VTM GO. self._kodi.show_listing(listing, 30019, content='episodes', sort='label')
class Menu: """ Menu code """ def __init__(self, kodi): """ Initialise object :type kodi: KodiWrapper """ self._kodi = kodi self._vtm_go = VtmGo(self._kodi) def show_mainmenu(self): """ Show the main menu """ kids = self._kodi.kids_mode() listing = [] listing.append( TitleItem( title=self._kodi.localize(30001), # A-Z path=self._kodi.url_for('show_catalog_all', kids=kids), art_dict=dict(icon='DefaultMovieTitle.png'), info_dict=dict(plot=self._kodi.localize(30002), ))) listing.append( TitleItem( title=self._kodi.localize(30003), # Catalogue path=self._kodi.url_for('show_catalog', kids=kids), art_dict=dict(icon='DefaultGenre.png'), info_dict=dict(plot=self._kodi.localize(30004), ))) listing.append( TitleItem( title=self._kodi.localize(30007), # TV Channels path=self._kodi.url_for('show_channels', kids=kids), art_dict=dict(icon='DefaultAddonPVRClient.png'), info_dict=dict(plot=self._kodi.localize(30008), ))) if self._kodi.get_setting_as_bool('interface_show_recommendations'): listing.append( TitleItem( title=self._kodi.localize(30015), # Recommendations path=self._kodi.url_for('show_recommendations', kids=kids), art_dict={'icon': 'DefaultFavourites.png'}, info_dict={ 'plot': self._kodi.localize(30016), })) if self._kodi.get_setting_as_bool('interface_show_mylist'): listing.append( TitleItem( title=self._kodi.localize(30017), # My List path=self._kodi.url_for('show_mylist', kids=kids), art_dict={'icon': 'DefaultPlaylist.png'}, info_dict={ 'plot': self._kodi.localize(30018), })) if self._kodi.get_setting_as_bool('interface_show_continuewatching'): listing.append( TitleItem( title=self._kodi.localize(30019), # Continue watching path=self._kodi.url_for('show_continuewatching', kids=kids), art_dict={'icon': 'DefaultInProgressShows.png'}, info_dict={ 'plot': self._kodi.localize(30020), })) listing.append( TitleItem( title=self._kodi.localize(30009), # Search path=self._kodi.url_for('show_search', kids=kids), art_dict=dict(icon='DefaultAddonsSearch.png'), info_dict=dict(plot=self._kodi.localize(30010), ))) if self._kodi.get_setting_as_bool( 'interface_show_kids_zone') and not kids: listing.append( TitleItem( title=self._kodi.localize(30011), # Kids Zone path=self._kodi.url_for('show_main_menu', kids=True), art_dict=dict(icon='DefaultUser.png'), info_dict=dict(plot=self._kodi.localize(30012), ))) self._kodi.show_listing(listing) def check_credentials(self): """ Check credentials (called from settings) """ try: from resources.lib.vtmgo.vtmgoauth import VtmGoAuth auth = VtmGoAuth(self._kodi) auth.clear_token() auth.get_token() self._kodi.show_ok_dialog( message=self._kodi.localize(30202)) # Credentials are correct! except InvalidLoginException: self._kodi.show_ok_dialog(message=self._kodi.localize( 30203)) # Your credentials are not valid! except LoginErrorException as e: self._kodi.show_ok_dialog(message=self._kodi.localize( 30702, code=e.code)) # Unknown error while logging in: {code} self._kodi.open_settings() def format_plot(self, obj): """ Format the plot for a item :type obj: object :rtype str """ plot = '' if hasattr(obj, 'description'): plot += obj.description plot += '\n\n' if hasattr(obj, 'epg'): if obj.epg: plot += self._kodi.localize( 30213, # Now start=obj.epg[0].start.strftime('%H:%M'), end=obj.epg[0].end.strftime('%H:%M'), title=obj.epg[0].title) + "\n" if len(obj.epg) > 1: plot += self._kodi.localize( 30214, # Next start=obj.epg[1].start.strftime('%H:%M'), end=obj.epg[1].end.strftime('%H:%M'), title=obj.epg[1].title) + "\n" # Add remaining if hasattr(obj, 'remaining') and obj.remaining is not None: if obj.remaining == 0: plot += '» ' + self._kodi.localize( 30208) + "\n" # Available until midnight elif obj.remaining == 1: plot += '» ' + self._kodi.localize( 30209) + "\n" # One more day remaining elif obj.remaining / 365 > 5: pass # If it is available for more than 5 years, do not show elif obj.remaining / 365 > 2: plot += '» ' + self._kodi.localize( 30210, years=int( obj.remaining / 365)) + "\n" # X years remaining elif obj.remaining / 30.5 > 3: plot += '» ' + self._kodi.localize( 30211, months=int( obj.remaining / 30.5)) + "\n" # X months remaining else: plot += '» ' + self._kodi.localize( 30212, days=obj.remaining) + "\n" # X days remaining # Add geo-blocked message if hasattr(obj, 'geoblocked') and obj.geoblocked: plot += self._kodi.localize(30207) # Geo-blocked plot += '\n' return plot.rstrip() def generate_titleitem(self, item, progress=False): """ Generate a TitleItem based on a Movie, Program or Episode. :type item: Union[Movie, Program, Episode] :type progress: bool :rtype TitleItem """ art_dict = { 'thumb': item.cover, } info_dict = { 'title': item.name, 'plot': item.description, } prop_dict = {} # # Movie # if isinstance(item, Movie): if item.my_list: context_menu = [( self._kodi.localize(30101), # Remove from My List 'XBMC.Container.Update(%s)' % self._kodi.url_for( 'mylist_del', kids=self._kodi.kids_mode(), video_type=self._vtm_go.CONTENT_TYPE_MOVIE, content_id=item.movie_id))] else: context_menu = [( self._kodi.localize(30100), # Add to My List 'XBMC.Container.Update(%s)' % self._kodi.url_for( 'mylist_add', kids=self._kodi.kids_mode(), video_type=self._vtm_go.CONTENT_TYPE_MOVIE, content_id=item.movie_id))] art_dict.update({ 'fanart': item.cover, }) info_dict.update({ 'mediatype': 'movie', 'plot': self.format_plot(item), 'duration': item.duration, 'year': item.year, 'aired': item.aired, 'studio': CHANNELS.get(item.channel, {}).get('studio_icon'), 'mpaa': ', '.join(item.legal) if hasattr(item, 'legal') and item.legal else self._kodi.localize(30216), }) return TitleItem(title=item.name, path=self._kodi.url_for('play', category='movies', item=item.movie_id), art_dict=art_dict, info_dict=info_dict, stream_dict={ 'codec': 'h264', 'height': 1080, 'width': 1920, }, context_menu=context_menu, is_playable=True) # # Program # if isinstance(item, Program): if item.my_list: context_menu = [( self._kodi.localize(30101), # Remove from My List 'XBMC.Container.Update(%s)' % self._kodi.url_for( 'mylist_del', kids=self._kodi.kids_mode(), video_type=self._vtm_go.CONTENT_TYPE_PROGRAM, content_id=item.program_id))] else: context_menu = [( self._kodi.localize(30100), # Add to My List 'XBMC.Container.Update(%s)' % self._kodi.url_for( 'mylist_add', kids=self._kodi.kids_mode(), video_type=self._vtm_go.CONTENT_TYPE_PROGRAM, content_id=item.program_id))] art_dict.update({ 'fanart': item.cover, 'banner': item.cover, }) info_dict.update({ 'mediatype': None, 'title': item.name, 'plot': self.format_plot(item), 'studio': CHANNELS.get(item.channel, {}).get('studio_icon'), 'mpaa': ', '.join(item.legal) if hasattr(item, 'legal') and item.legal else self._kodi.localize(30216), 'season': len(item.seasons), }) return TitleItem(title=item.name, path=self._kodi.url_for('show_catalog_program', program=item.program_id), art_dict=art_dict, info_dict=info_dict, context_menu=context_menu) # # Episode # if isinstance(item, Episode): context_menu = [( self._kodi.localize(30102), # Go to Program 'XBMC.Container.Update(%s)' % self._kodi.url_for( 'show_catalog_program', program=item.program_id))] info_dict.update({ 'tvshowtitle': item.program_name, 'title': item.name, 'plot': self.format_plot(item), 'duration': item.duration, 'season': item.season, 'episode': item.number, 'mediatype': 'episode', 'set': item.program_name, 'studio': item.channel, 'aired': item.aired, 'mpaa': ', '.join(item.legal) if hasattr(item, 'legal') and item.legal else self._kodi.localize(30216), }) if progress and item.watched: info_dict.update({ 'playcount': 1, }) stream_dict = { 'codec': 'h264', 'duration': item.duration, 'height': 1080, 'width': 1920, } # Get program and episode details from cache program = self._vtm_go.get_program(item.program_id, cache=True) if program: episode = self._vtm_go.get_episode_from_program( program, item.episode_id) if episode: art_dict.update({ 'fanart': episode.cover, 'banner': episode.cover, }) info_dict.update({ 'tvshowtitle': program.name, 'title': episode.name, 'plot': self.format_plot(episode), 'duration': episode.duration, 'season': episode.season, 'episode': episode.number, 'set': program.name, 'studio': episode.channel, 'aired': episode.aired, 'mpaa': ', '.join(episode.legal) if hasattr(episode, 'legal') and episode.legal else self._kodi.localize(30216), }) if progress and item.watched: info_dict.update({ 'playcount': 1, }) stream_dict.update({ 'duration': episode.duration, }) # Add progress info if progress and not item.watched and item.progress: prop_dict.update({ 'ResumeTime': item.progress, 'TotalTime': item.progress + 1, }) return TitleItem(title=info_dict['title'], path=self._kodi.url_for('play', category='episodes', item=item.episode_id), art_dict=art_dict, info_dict=info_dict, stream_dict=stream_dict, prop_dict=prop_dict, context_menu=context_menu, is_playable=True) raise Exception('Unknown video_type')