def store(store_location): """ Returns the Singleton store object for the given type :param str|unicode store_location: Either the Kodi (KODI) store or in the Retrospect (LOCAL) store :return: An instance of the setting store :rtype: settingsstore.SettingsStore """ store = AddonSettings.__setting_stores.get(store_location, None) if store is not None: return store with AddonSettings.__settings_lock: # Just a double check in case there was a race condition?? store = AddonSettings.__setting_stores.get(store_location, None) if store is not None: return store if store_location == KODI: store = kodisettings.KodiSettings(Logger.instance()) elif store_location == LOCAL: store = localsettings.LocalSettings(Config.profileDir, Logger.instance()) else: raise IndexError("Cannot find Setting store type: {0}".format(store_location)) AddonSettings.__setting_stores[store_location] = store return store
def __send_log(self): """ Send log files via Pastbin or Gist. """ from resources.lib.helpers.logsender import LogSender sender_mode = 'hastebin' log_sender = LogSender(Config.logSenderApi, logger=Logger.instance(), mode=sender_mode) try: title = LanguageHelper.get_localized_string( LanguageHelper.LogPostSuccessTitle) url_text = LanguageHelper.get_localized_string( LanguageHelper.LogPostLogUrl) files_to_send = [ Logger.instance().logFileName, Logger.instance().logFileName.replace(".log", ".old.log") ] if sender_mode != "gist": paste_url = log_sender.send_file(Config.logFileNameAddon, files_to_send[0]) else: paste_url = log_sender.send_files(Config.logFileNameAddon, files_to_send) XbmcWrapper.show_dialog(title, url_text % (paste_url, )) except Exception as e: Logger.error("Error sending %s", Config.logFileNameAddon, exc_info=True) title = LanguageHelper.get_localized_string( LanguageHelper.LogPostErrorTitle) error_text = LanguageHelper.get_localized_string( LanguageHelper.LogPostError) error = error_text % (str(e), ) XbmcWrapper.show_dialog(title, error.strip(": "))
def __exit__(self, exc_type, exc_val, exc_tb): if exc_val: Logger.critical("Error in menu handling: %s", str(exc_val), exc_info=True) # make sure we leave no references behind AddonSettings.clear_cached_addon_settings_object() # close the log to prevent locking on next call Logger.instance().close_log() return False
class LogAction(AddonAction): def __init__(self, parameter_parser): super(LogAction, self).__init__(parameter_parser) @LockWithDialog(logger=Logger.instance()) def execute(self): """ Send log files via Pastbin or Gist. """ from resources.lib.helpers.logsender import LogSender sender_mode = 'hastebin' log_sender = LogSender(Config.logSenderApi, logger=Logger.instance(), mode=sender_mode) try: title = LanguageHelper.get_localized_string(LanguageHelper.LogPostSuccessTitle) url_text = LanguageHelper.get_localized_string(LanguageHelper.LogPostLogUrl) files_to_send = [Logger.instance().logFileName, Logger.instance().logFileName.replace(".log", ".old.log")] paste_url = log_sender.send_file(Config.logFileNameAddon, files_to_send[0]) XbmcWrapper.show_dialog(title, url_text % (paste_url,)) except Exception as e: Logger.error("Error sending %s", Config.logFileNameAddon, exc_info=True) title = LanguageHelper.get_localized_string(LanguageHelper.LogPostErrorTitle) error_text = LanguageHelper.get_localized_string(LanguageHelper.LogPostError) error = error_text % (str(e),) XbmcWrapper.show_dialog(title, error.strip(": "))
def toggle_cloak(self): """ Toggles the cloaking (showing/hiding) of the selected folder. """ item = self._pickler.de_pickle_media_item( self.params[self.keywordPickle]) Logger.info("Cloaking current item: %s", item) c = Cloaker(self.channelObject, AddonSettings.store(LOCAL), logger=Logger.instance()) if c.is_cloaked(item.url): c.un_cloak(item.url) self.refresh() return first_time = c.cloak(item.url) if first_time: XbmcWrapper.show_dialog( LanguageHelper.get_localized_string( LanguageHelper.CloakFirstTime), LanguageHelper.get_localized_string( LanguageHelper.CloakMessage)) del c self.refresh()
def execute(self): title = LanguageHelper.get_localized_string( LanguageHelper.CleanupCache)[:-1] clean = \ XbmcWrapper.show_yes_no(title, LanguageHelper.CleanupConfirmation) if not clean: Logger.warning("Clean-up cancelled") return files_to_remove = { "channelindex.json": "Cleaning: Channel Index", "cookiejar.dat": "Cleaning: Cookies in cookiejar.dat", "xot.session.lock": "Cleaning: Session lock" } for file_name, log_line in files_to_remove.items(): Logger.info(log_line) files_to_remove = os.path.join(Config.profileDir, file_name) if os.path.isfile(files_to_remove): os.remove(files_to_remove) Logger.info("Cleaning: PickeStore objects") self.parameter_parser.pickler.purge_store(Config.addonId, age=0) Logger.info("Cleaning: Cache objects in cache folder") env_ctrl = EnvController(Logger.instance()) env_ctrl.cache_clean_up(Config.cacheDir, 0)
def update_video_api_item(self, item): """ Updates an existing MediaItem with more data. Used to update none complete MediaItems (self.complete = False). This could include opening the item's URL to fetch more data and then process that data or retrieve it's real media-URL. The method should at least: * cache the thumbnail to disk (use self.noImage if no thumb is available). * set at least one MediaItemPart with a single MediaStream. * set self.complete = True. if the returned item does not have a MediaItemPart then the self.complete flag will automatically be set back to False. :param MediaItem item: the original MediaItem that needs updating. :return: The original item with more data added to it's properties. :rtype: MediaItem """ Logger.debug('Starting UpdateChannelItem for %s (%s)', item.name, self.channelName) data = UriHandler.open(item.url, proxy=self.proxy) json = JsonHelper(data, logger=Logger.instance()) videos = json.get_value("videoReferences") subtitles = json.get_value("subtitleReferences") Logger.trace(videos) return self.__update_item_from_video_references( item, videos, subtitles)
def __init__(self, addon_name, handle, params): """ :param str addon_name: The name of the add-on :param int handle: The handle for this run :param str params: The parameters used to start the ActionParser """ Logger.debug("Parsing parameters from: %s", params) self.handle = int(handle) # determine the query parameters self._params = params self.params = self.__get_parameters(params) self.pluginName = addon_name # We need a picker for this instance self.pickler = Pickler(Config.profileDir) # Field for property self.__media_item = None # For remote debugging and log reading purpose we need the full pickle string. if Logger.instance().minLogLevel <= Logger.LVL_DEBUG \ and self.media_item is not None \ and self.pickler.is_pickle_store_id(self.params[keyword.PICKLE]): Logger.debug("Replacing PickleStore pickle '%s' with full pickle", self.params[keyword.PICKLE]) self.params[keyword.PICKLE] = self.pickler.pickle_media_item(self.media_item)
def update_video_item(self, item): """ Updates an existing MediaItem with more data. Used to update none complete MediaItems (self.complete = False). This could include opening the item's URL to fetch more data and then process that data or retrieve it's real media-URL. The method should at least: * cache the thumbnail to disk (use self.noImage if no thumb is available). * set at least one MediaItemPart with a single MediaStream. * set self.complete = True. if the returned item does not have a MediaItemPart then the self.complete flag will automatically be set back to False. :param MediaItem item: the original MediaItem that needs updating. :return: The original item with more data added to it's properties. :rtype: MediaItem """ Logger.debug('Starting update_video_item for %s (%s)', item.name, self.channelName) if not item.url.endswith(".js"): data = UriHandler.open(item.url) data_id = Regexer.do_regex(r'data-id="(\d+)"[^>]+data-playout', data) if data_id is None: Logger.warning("Cannot find stream-id for L1 stream.") return item data_url = "https://limburg.bbvms.com/p/L1_video/c/{}.json".format(data_id[0]) else: data_url = item.url data = UriHandler.open(data_url) json = JsonHelper(data, logger=Logger.instance()) Logger.trace(json) base_url = json.get_value("publicationData", "defaultMediaAssetPath") streams = json.get_value("clipData", "assets") item.MediaItemParts = [] part = item.create_new_empty_media_part() for stream in streams: url = stream.get("src", None) if "://" not in url: url = "{}{}".format(base_url, url) bitrate = stream.get("bandwidth", None) if url: part.append_media_stream(url, bitrate) if not item.thumb and json.get_value("thumbnails"): url = json.get_value("thumbnails")[0].get("src", None) if url and "http:/" not in url: url = "%s%s" % (self.baseUrl, url) item.thumb = url item.complete = True return item
def log_on(self): """ Logs on to a website, using an url. First checks if the channel requires log on. If so and it's not already logged on, it should handle the log on. That part should be implemented by the specific channel. More arguments can be passed on, but must be handled by custom code. After a successful log on the self.loggedOn property is set to True and True is returned. :return: indication if the login was successful. :rtype: bool """ if self.__idToken: return True # check if there is a refresh token # refresh token: viervijfzes_refresh_token refresh_token = AddonSettings.get_setting("viervijfzes_refresh_token") client = AwsIdp("eu-west-1_dViSsKM5Y", "6s1h851s8uplco5h6mqh1jac8m", proxy=self.proxy, logger=Logger.instance()) if refresh_token: id_token = client.renew_token(refresh_token) if id_token: self.__idToken = id_token return True else: Logger.info("Extending token for VierVijfZes failed.") # username: viervijfzes_username username = AddonSettings.get_setting("viervijfzes_username") # password: viervijfzes_password v = Vault() password = v.get_setting("viervijfzes_password") if not username or not password: XbmcWrapper.show_dialog( title=None, lines=LanguageHelper.get_localized_string( LanguageHelper.MissingCredentials), ) return False id_token, refresh_token = client.authenticate(username, password) if not id_token or not refresh_token: Logger.error("Error getting a new token. Wrong password?") return False self.__idToken = id_token AddonSettings.set_setting("viervijfzes_refresh_token", refresh_token) return True
def __requests(self, uri, proxy, params, data, json, referer, additional_headers, no_cache, stream): with requests.session() as s: s.cookies = self.cookieJar s.verify = not self.ignoreSslErrors if self.cacheStore and not no_cache: Logger.trace("Adding the %s to the request", self.cacheStore) s.mount("https://", CacheHTTPAdapter(self.cacheStore)) s.mount("http://", CacheHTTPAdapter(self.cacheStore)) proxies = self.__get_proxies(proxy, uri) if proxies is not None and "dns" in proxies: s.mount("https://", DnsResolverHTTPAdapter(uri, proxies["dns"], logger=Logger.instance())) headers = self.__get_headers(referer, additional_headers) if params is not None: # Old UriHandler behaviour. Set form header to keep compatible if "content-type" not in headers: headers["content-type"] = "application/x-www-form-urlencoded" Logger.info("Performing a POST with '%s' for %s", headers["content-type"], uri) r = s.post(uri, data=params, proxies=proxies, headers=headers, stream=stream, timeout=self.webTimeOut) elif data is not None: # Normal Requests compatible data object Logger.info("Performing a POST with '%s' for %s", headers.get("content-type", "<No Content-Type>"), uri) r = s.post(uri, data=data, proxies=proxies, headers=headers, stream=stream, timeout=self.webTimeOut) elif json is not None: Logger.info("Performing a json POST with '%s' for %s", headers.get("content-type", "<No Content-Type>"), uri) r = s.post(uri, json=json, proxies=proxies, headers=headers, stream=stream, timeout=self.webTimeOut) else: Logger.info("Performing a GET for %s", uri) r = s.get(uri, proxies=proxies, headers=headers, stream=stream, timeout=self.webTimeOut) if r.ok: Logger.info("%s resulted in '%s %s' (%s) for %s", r.request.method, r.status_code, r.reason, r.elapsed, r.url) else: Logger.error("%s failed with '%s %s' (%s) for %s", r.request.method, r.status_code, r.reason, r.elapsed, r.url) self.status = UriStatus(code=r.status_code, url=r.url, error=not r.ok, reason=r.reason) if self.cookieJarFile: # noinspection PyUnresolvedReferences self.cookieJar.save() return r
def __get_index(self): """ Loads the channel index and if there is none, makes sure one is created. Checks: 1. Existence of the index 2. Channel add-ons in the index vs actual add-ons :return: The current channel index. :rtype: dict """ # if it was not already re-index and the bit was set if self.__reindex: if self.__reindexed: Logger.warning( "Forced re-index set, but a re-index was already done previously. Not Rebuilding." ) else: Logger.info("Forced re-index set. Rebuilding.") return self.__rebuild_index() if not os.path.isfile(self.__CHANNEL_INDEX): Logger.info("No index file found at '%s'. Rebuilding.", self.__CHANNEL_INDEX) return self.__rebuild_index() try: with io.open(self.__CHANNEL_INDEX, 'rt', encoding='utf-8') as fd: data = fd.read() index_json = JsonHelper(data, logger=Logger.instance()) Logger.debug("Loaded index from '%s'.", self.__CHANNEL_INDEX) if not self.__is_index_consistent(index_json.json): return self.__rebuild_index() return index_json.json except: Logger.critical("Error reading channel index. Rebuilding.", exc_info=True) return self.__rebuild_index()
def update_video_item(self, item): """ Updates an existing MediaItem with more data. Used to update none complete MediaItems (self.complete = False). This could include opening the item's URL to fetch more data and then process that data or retrieve it's real media-URL. The method should at least: * cache the thumbnail to disk (use self.noImage if no thumb is available). * set at least one MediaItemPart with a single MediaStream. * set self.complete = True. if the returned item does not have a MediaItemPart then the self.complete flag will automatically be set back to False. :param MediaItem item: the original MediaItem that needs updating. :return: The original item with more data added to it's properties. :rtype: MediaItem """ Logger.debug('Starting update_video_item for %s (%s)', item.name, self.channelName) # we need to fetch the actual url as it might differ for single video items data, secure_url = UriHandler.header(item.url, proxy=self.proxy) # Get the MZID secure_url = secure_url.rstrip("/") secure_url = "%s.mssecurevideo.json" % (secure_url, ) data = UriHandler.open(secure_url, proxy=self.proxy, additional_headers=item.HttpHeaders) secure_data = JsonHelper(data, logger=Logger.instance()) mzid = secure_data.get_value( list(secure_data.json.keys())[0], "videoid") return self.update_video_for_mzid(item, mzid)
def process_folder_list(self, favorites=None): """Wraps the channel.process_folder_list :param list[MediaItem]|None favorites: """ Logger.info("Plugin::process_folder_list Doing process_folder_list") try: ok = True # read the item from the parameters selected_item = self.media_item # determine the parent guid parent_guid = self._get_parent_guid(self.channelObject, selected_item) if favorites is None: watcher = StopWatch("Plugin process_folder_list", Logger.instance()) media_items = self.channelObject.process_folder_list( selected_item) watcher.lap("Class process_folder_list finished") else: watcher = StopWatch("Plugin process_folder_list With Items", Logger.instance()) media_items = favorites if len(media_items) == 0: Logger.warning("process_folder_list returned %s items", len(media_items)) ok = self.__show_empty_information(media_items, favs=favorites is not None) else: Logger.debug("process_folder_list returned %s items", len(media_items)) kodi_items = [] for media_item in media_items: # type: MediaItem self.__update_artwork(media_item, self.channelObject) if media_item.type == 'folder' or media_item.type == 'append' or media_item.type == "page": action = self.actionListFolder folder = True elif media_item.is_playable(): action = self.actionPlayVideo folder = False else: Logger.critical( "Plugin::process_folder_list: Cannot determine what to add" ) continue # Get the Kodi item kodi_item = media_item.get_kodi_item() self.__set_kodi_properties(kodi_item, media_item, folder, is_favourite=favorites is not None) # Get the context menu items context_menu_items = self.__get_context_menu_items( self.channelObject, item=media_item) kodi_item.addContextMenuItems(context_menu_items) # Get the action URL url = media_item.actionUrl if url is None: url = self._create_action_url(self.channelObject, action=action, item=media_item, store_id=parent_guid) # Add them to the list of Kodi items kodi_items.append((url, kodi_item, folder)) watcher.lap("Kodi Items generated") # add items but if OK was False, keep it like that ok = ok and xbmcplugin.addDirectoryItems(self.handle, kodi_items, len(kodi_items)) watcher.lap("items send to Kodi") if ok and parent_guid is not None: self._pickler.store_media_items(parent_guid, selected_item, media_items) watcher.stop() self.__add_sort_method_to_handle(self.handle, media_items) self.__add_breadcrumb(self.handle, self.channelObject, selected_item) # set the content. It needs to be "episodes" to make the MediaItem.set_season_info() work xbmcplugin.setContent(handle=self.handle, content="episodes") xbmcplugin.endOfDirectory(self.handle, ok) except Exception: Logger.error("Plugin::Error Processing FolderList", exc_info=True) XbmcWrapper.show_notification( LanguageHelper.get_localized_string(LanguageHelper.ErrorId), LanguageHelper.get_localized_string(LanguageHelper.ErrorList), XbmcWrapper.Error, 4000) xbmcplugin.endOfDirectory(self.handle, False)
def __init__(self, addon_name, params, handle=0): # NOSONAR complexity """ Initialises the plugin with given arguments. :param str addon_name: The add-on name. :param str params: The input parameters from the query string. :param int handle: The Kodi directory handle. """ Logger.info("******** Starting %s add-on version %s/repo *********", Config.appName, Config.version) # noinspection PyTypeChecker super(Plugin, self).__init__(addon_name, handle, params) Logger.debug(self) # Container Properties self.propertyRetrospect = "Retrospect" self.propertyRetrospectChannel = "RetrospectChannel" self.propertyRetrospectChannelSetting = "RetrospectChannelSettings" self.propertyRetrospectFolder = "RetrospectFolder" self.propertyRetrospectVideo = "RetrospectVideo" self.propertyRetrospectCloaked = "RetrospectCloaked" self.propertyRetrospectCategory = "RetrospectCategory" self.propertyRetrospectFavorite = "RetrospectFavorite" self.propertyRetrospectAdaptive = "RetrospectAdaptive" # channel objects self.channelObject = None self.channelFile = "" self.channelCode = None self.methodContainer = dict( ) # : storage for the inspect.getmembers(channel) method. Improves performance # are we in session? session_active = SessionHelper.is_session_active(Logger.instance()) # fetch some environment settings env_ctrl = envcontroller.EnvController(Logger.instance()) if not session_active: # do add-on start stuff Logger.info("Add-On start detected. Performing startup actions.") # print the folder structure env_ctrl.print_retrospect_settings_and_folders( Config, AddonSettings) # show notification XbmcWrapper.show_notification(None, LanguageHelper.get_localized_string( LanguageHelper.StartingAddonId) % (Config.appName, ), fallback=False, logger=Logger) # check for updates. Using local import for performance from resources.lib.updater import Updater up = Updater(Config.updateUrl, Config.version, UriHandler.instance(), Logger.instance(), AddonSettings.get_release_track()) if up.is_new_version_available(): Logger.info("Found new version online: %s vs %s", up.currentVersion, up.onlineVersion) notification = LanguageHelper.get_localized_string( LanguageHelper.NewVersion2Id) notification = notification % (Config.appName, up.onlineVersion) XbmcWrapper.show_notification(None, lines=notification, display_time=20000) # check for cache folder env_ctrl.cache_check() # do some cache cleanup env_ctrl.cache_clean_up(Config.cacheDir, Config.cacheValidTime) # empty picklestore self._pickler.purge_store(Config.addonId) # create a session SessionHelper.create_session(Logger.instance()) #=============================================================================== # Start the plugin version of progwindow #=============================================================================== if len(self.params) == 0: # Show initial start if not in a session # now show the list if AddonSettings.show_categories(): self.show_categories() else: self.show_channel_list() #=============================================================================== # Start the plugin verion of the episode window #=============================================================================== else: # Determine what stage we are in. Check that there are more than 2 Parameters if len(self.params) > 1 and self.keywordChannel in self.params: # retrieve channel characteristics self.channelFile = os.path.splitext( self.params[self.keywordChannel])[0] self.channelCode = self.params[self.keywordChannelCode] Logger.debug( "Found Channel data in URL: channel='%s', code='%s'", self.channelFile, self.channelCode) # import the channel channel_register = ChannelIndex.get_register() channel = channel_register.get_channel(self.channelFile, self.channelCode) if channel is not None: self.channelObject = channel else: Logger.critical( "None or more than one channels were found, unable to continue." ) return # init the channel as plugin self.channelObject.init_channel() Logger.info("Loaded: %s", self.channelObject.channelName) elif self.keywordCategory in self.params \ or self.keywordAction in self.params and ( self.params[self.keywordAction] == self.actionAllFavourites or self.params[self.keywordAction] == self.actionRemoveFavourite): # no channel needed for these favourites actions. pass # =============================================================================== # Vault Actions # =============================================================================== elif self.keywordAction in self.params and \ self.params[self.keywordAction] in \ ( self.actionSetEncryptedValue, self.actionSetEncryptionPin, self.actionResetVault ): try: # Import vault here, as it is only used here or in a channel # that supports it from resources.lib.vault import Vault action = self.params[self.keywordAction] if action == self.actionResetVault: Vault.reset() return v = Vault() if action == self.actionSetEncryptionPin: v.change_pin() elif action == self.actionSetEncryptedValue: v.set_setting( self.params[self.keywordSettingId], self.params.get(self.keywordSettingName, ""), self.params.get(self.keywordSettingActionId, None)) finally: if self.keywordSettingTabFocus in self.params: AddonSettings.show_settings( self.params[self.keywordSettingTabFocus], self.params.get(self.keywordSettingSettingFocus, None)) return elif self.keywordAction in self.params and \ self.actionPostLog in self.params[self.keywordAction]: self.__send_log() return elif self.keywordAction in self.params and \ self.actionProxy in self.params[self.keywordAction]: # do this here to not close the busy dialog on the SetProxy when # a confirm box is shown title = LanguageHelper.get_localized_string( LanguageHelper.ProxyChangeConfirmTitle) content = LanguageHelper.get_localized_string( LanguageHelper.ProxyChangeConfirm) if not XbmcWrapper.show_yes_no(title, content): Logger.warning( "Stopping proxy update due to user intervention") return language = self.params.get(self.keywordLanguage, None) proxy_id = self.params.get(self.keywordProxy, None) local_ip = self.params.get(self.keywordLocalIP, None) self.__set_proxy(language, proxy_id, local_ip) return else: Logger.critical("Error determining Plugin action") return #=============================================================================== # See what needs to be done. #=============================================================================== if self.keywordAction not in self.params: Logger.critical( "Action parameters missing from request. Parameters=%s", self.params) return elif self.params[self.keywordAction] == self.actionListCategory: self.show_channel_list(self.params[self.keywordCategory]) elif self.params[ self.keywordAction] == self.actionConfigureChannel: self.__configure_channel(self.channelObject) elif self.params[self.keywordAction] == self.actionFavourites: # we should show the favourites self.show_favourites(self.channelObject) elif self.params[self.keywordAction] == self.actionAllFavourites: self.show_favourites(None) elif self.params[self.keywordAction] == self.actionListFolder: # channelName and URL is present, Parse the folder self.process_folder_list() elif self.params[self.keywordAction] == self.actionPlayVideo: self.play_video_item() elif not self.params[self.keywordAction] == "": self.on_action_from_context_menu( self.params[self.keywordAction]) else: Logger.warning( "Number of parameters (%s) or parameter (%s) values not implemented", len(self.params), self.params) self.__fetch_textures() return
class Plugin(ParameterParser): """ Main Plugin Class This class makes it possible to access all the XOT channels as a Kodi Add-on instead of a script. """ def __init__(self, addon_name, params, handle=0): # NOSONAR complexity """ Initialises the plugin with given arguments. :param str addon_name: The add-on name. :param str params: The input parameters from the query string. :param int handle: The Kodi directory handle. """ Logger.info("******** Starting %s add-on version %s/repo *********", Config.appName, Config.version) # noinspection PyTypeChecker super(Plugin, self).__init__(addon_name, handle, params) Logger.debug(self) # Container Properties self.propertyRetrospect = "Retrospect" self.propertyRetrospectChannel = "RetrospectChannel" self.propertyRetrospectChannelSetting = "RetrospectChannelSettings" self.propertyRetrospectFolder = "RetrospectFolder" self.propertyRetrospectVideo = "RetrospectVideo" self.propertyRetrospectCloaked = "RetrospectCloaked" self.propertyRetrospectCategory = "RetrospectCategory" self.propertyRetrospectFavorite = "RetrospectFavorite" self.propertyRetrospectAdaptive = "RetrospectAdaptive" # channel objects self.channelObject = None self.channelFile = "" self.channelCode = None self.methodContainer = dict( ) # : storage for the inspect.getmembers(channel) method. Improves performance # are we in session? session_active = SessionHelper.is_session_active(Logger.instance()) # fetch some environment settings env_ctrl = envcontroller.EnvController(Logger.instance()) if not session_active: # do add-on start stuff Logger.info("Add-On start detected. Performing startup actions.") # print the folder structure env_ctrl.print_retrospect_settings_and_folders( Config, AddonSettings) # show notification XbmcWrapper.show_notification(None, LanguageHelper.get_localized_string( LanguageHelper.StartingAddonId) % (Config.appName, ), fallback=False, logger=Logger) # check for updates. Using local import for performance from resources.lib.updater import Updater up = Updater(Config.updateUrl, Config.version, UriHandler.instance(), Logger.instance(), AddonSettings.get_release_track()) if up.is_new_version_available(): Logger.info("Found new version online: %s vs %s", up.currentVersion, up.onlineVersion) notification = LanguageHelper.get_localized_string( LanguageHelper.NewVersion2Id) notification = notification % (Config.appName, up.onlineVersion) XbmcWrapper.show_notification(None, lines=notification, display_time=20000) # check for cache folder env_ctrl.cache_check() # do some cache cleanup env_ctrl.cache_clean_up(Config.cacheDir, Config.cacheValidTime) # empty picklestore self._pickler.purge_store(Config.addonId) # create a session SessionHelper.create_session(Logger.instance()) #=============================================================================== # Start the plugin version of progwindow #=============================================================================== if len(self.params) == 0: # Show initial start if not in a session # now show the list if AddonSettings.show_categories(): self.show_categories() else: self.show_channel_list() #=============================================================================== # Start the plugin verion of the episode window #=============================================================================== else: # Determine what stage we are in. Check that there are more than 2 Parameters if len(self.params) > 1 and self.keywordChannel in self.params: # retrieve channel characteristics self.channelFile = os.path.splitext( self.params[self.keywordChannel])[0] self.channelCode = self.params[self.keywordChannelCode] Logger.debug( "Found Channel data in URL: channel='%s', code='%s'", self.channelFile, self.channelCode) # import the channel channel_register = ChannelIndex.get_register() channel = channel_register.get_channel(self.channelFile, self.channelCode) if channel is not None: self.channelObject = channel else: Logger.critical( "None or more than one channels were found, unable to continue." ) return # init the channel as plugin self.channelObject.init_channel() Logger.info("Loaded: %s", self.channelObject.channelName) elif self.keywordCategory in self.params \ or self.keywordAction in self.params and ( self.params[self.keywordAction] == self.actionAllFavourites or self.params[self.keywordAction] == self.actionRemoveFavourite): # no channel needed for these favourites actions. pass # =============================================================================== # Vault Actions # =============================================================================== elif self.keywordAction in self.params and \ self.params[self.keywordAction] in \ ( self.actionSetEncryptedValue, self.actionSetEncryptionPin, self.actionResetVault ): try: # Import vault here, as it is only used here or in a channel # that supports it from resources.lib.vault import Vault action = self.params[self.keywordAction] if action == self.actionResetVault: Vault.reset() return v = Vault() if action == self.actionSetEncryptionPin: v.change_pin() elif action == self.actionSetEncryptedValue: v.set_setting( self.params[self.keywordSettingId], self.params.get(self.keywordSettingName, ""), self.params.get(self.keywordSettingActionId, None)) finally: if self.keywordSettingTabFocus in self.params: AddonSettings.show_settings( self.params[self.keywordSettingTabFocus], self.params.get(self.keywordSettingSettingFocus, None)) return elif self.keywordAction in self.params and \ self.actionPostLog in self.params[self.keywordAction]: self.__send_log() return elif self.keywordAction in self.params and \ self.actionProxy in self.params[self.keywordAction]: # do this here to not close the busy dialog on the SetProxy when # a confirm box is shown title = LanguageHelper.get_localized_string( LanguageHelper.ProxyChangeConfirmTitle) content = LanguageHelper.get_localized_string( LanguageHelper.ProxyChangeConfirm) if not XbmcWrapper.show_yes_no(title, content): Logger.warning( "Stopping proxy update due to user intervention") return language = self.params.get(self.keywordLanguage, None) proxy_id = self.params.get(self.keywordProxy, None) local_ip = self.params.get(self.keywordLocalIP, None) self.__set_proxy(language, proxy_id, local_ip) return else: Logger.critical("Error determining Plugin action") return #=============================================================================== # See what needs to be done. #=============================================================================== if self.keywordAction not in self.params: Logger.critical( "Action parameters missing from request. Parameters=%s", self.params) return elif self.params[self.keywordAction] == self.actionListCategory: self.show_channel_list(self.params[self.keywordCategory]) elif self.params[ self.keywordAction] == self.actionConfigureChannel: self.__configure_channel(self.channelObject) elif self.params[self.keywordAction] == self.actionFavourites: # we should show the favourites self.show_favourites(self.channelObject) elif self.params[self.keywordAction] == self.actionAllFavourites: self.show_favourites(None) elif self.params[self.keywordAction] == self.actionListFolder: # channelName and URL is present, Parse the folder self.process_folder_list() elif self.params[self.keywordAction] == self.actionPlayVideo: self.play_video_item() elif not self.params[self.keywordAction] == "": self.on_action_from_context_menu( self.params[self.keywordAction]) else: Logger.warning( "Number of parameters (%s) or parameter (%s) values not implemented", len(self.params), self.params) self.__fetch_textures() return def show_categories(self): """ Displays the show_categories that are currently available in XOT as a directory listing. :return: indication if all succeeded. :rtype: bool """ Logger.info("Plugin::show_categories") channel_register = ChannelIndex.get_register() categories = channel_register.get_categories() kodi_items = [] icon = Config.icon fanart = Config.fanart for category in categories: name = LanguageHelper.get_localized_category(category) kodi_item = xbmcgui.ListItem(name, name) # set art try: kodi_item.setIconImage(icon) except: # it was deprecated pass kodi_item.setArt({'thumb': icon, 'icon': icon}) kodi_item.setProperty(self.propertyRetrospect, "true") kodi_item.setProperty(self.propertyRetrospectCategory, "true") if not AddonSettings.hide_fanart(): kodi_item.setArt({'fanart': fanart}) url = self._create_action_url(None, action=self.actionListCategory, category=category) kodi_items.append((url, kodi_item, True)) # Logger.Trace(kodi_items) ok = xbmcplugin.addDirectoryItems(self.handle, kodi_items, len(kodi_items)) xbmcplugin.addSortMethod(handle=self.handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL) xbmcplugin.endOfDirectory(self.handle, ok) return ok def show_channel_list(self, category=None): """ Displays the channels that are currently available in XOT as a directory listing. :param str category: The category to show channels for """ if category: Logger.info("Plugin::show_channel_list for %s", category) else: Logger.info("Plugin::show_channel_list") try: # only display channels channel_register = ChannelIndex.get_register() channels = channel_register.get_channels() xbmc_items = [] # Should we show the "All Favourites"? if AddonSettings.show_show_favourites_in_channel_list(): icon = Config.icon fanart = Config.fanart name = LanguageHelper.get_localized_string( LanguageHelper.AllFavouritesId) kodi_item = xbmcgui.ListItem(name, name) # set art try: kodi_item.setIconImage(icon) except: # it was deprecated pass kodi_item.setArt({'thumb': icon, 'icon': icon}) kodi_item.setProperty(self.propertyRetrospect, "true") kodi_item.setProperty(self.propertyRetrospectCategory, "true") if not AddonSettings.hide_fanart(): kodi_item.setArt({'fanart': fanart}) url = self._create_action_url(None, action=self.actionAllFavourites) xbmc_items.append((url, kodi_item, True)) for channel in channels: if category and channel.category != category: Logger.debug("Skipping %s (%s) due to category filter", channel.channelName, channel.category) continue # Get the Kodi item item = channel.get_kodi_item() item.setProperty(self.propertyRetrospect, "true") item.setProperty(self.propertyRetrospectChannel, "true") if channel.settings: item.setProperty(self.propertyRetrospectChannelSetting, "true") if channel.adaptiveAddonSelectable: item.setProperty(self.propertyRetrospectAdaptive, "true") # Get the context menu items context_menu_items = self.__get_context_menu_items(channel) item.addContextMenuItems(context_menu_items) # Get the URL for the item url = self._create_action_url(channel, action=self.actionListFolder) # Append to the list of Kodi Items xbmc_items.append((url, item, True)) # Add the items ok = xbmcplugin.addDirectoryItems(self.handle, xbmc_items, len(xbmc_items)) # Just let Kodi display the order we give. xbmcplugin.addSortMethod( handle=self.handle, sortMethod=xbmcplugin.SORT_METHOD_UNSORTED) xbmcplugin.addSortMethod(handle=self.handle, sortMethod=xbmcplugin.SORT_METHOD_TITLE) xbmcplugin.addSortMethod(handle=self.handle, sortMethod=xbmcplugin.SORT_METHOD_GENRE) xbmcplugin.setContent(handle=self.handle, content="tvshows") xbmcplugin.endOfDirectory(self.handle, ok) except: xbmcplugin.endOfDirectory(self.handle, False) Logger.critical("Error fetching channels for plugin", exc_info=True) def show_favourites(self, channel): """ Show the favourites (for a channel). :param ChannelInfo|None channel: The channel to show favourites for. Might be None to show all. """ Logger.debug("Plugin::show_favourites") if channel is None: Logger.info("Showing all favourites") else: Logger.info("Showing favourites for: %s", channel) # Local import for performance from resources.lib.favourites import Favourites f = Favourites(Config.favouriteDir) favs = f.list(channel) self.process_folder_list(favs) def process_folder_list(self, favorites=None): """Wraps the channel.process_folder_list :param list[MediaItem]|None favorites: """ Logger.info("Plugin::process_folder_list Doing process_folder_list") try: ok = True # read the item from the parameters selected_item = self.media_item # determine the parent guid parent_guid = self._get_parent_guid(self.channelObject, selected_item) if favorites is None: watcher = StopWatch("Plugin process_folder_list", Logger.instance()) media_items = self.channelObject.process_folder_list( selected_item) watcher.lap("Class process_folder_list finished") else: watcher = StopWatch("Plugin process_folder_list With Items", Logger.instance()) media_items = favorites if len(media_items) == 0: Logger.warning("process_folder_list returned %s items", len(media_items)) ok = self.__show_empty_information(media_items, favs=favorites is not None) else: Logger.debug("process_folder_list returned %s items", len(media_items)) kodi_items = [] for media_item in media_items: # type: MediaItem self.__update_artwork(media_item, self.channelObject) if media_item.type == 'folder' or media_item.type == 'append' or media_item.type == "page": action = self.actionListFolder folder = True elif media_item.is_playable(): action = self.actionPlayVideo folder = False else: Logger.critical( "Plugin::process_folder_list: Cannot determine what to add" ) continue # Get the Kodi item kodi_item = media_item.get_kodi_item() self.__set_kodi_properties(kodi_item, media_item, folder, is_favourite=favorites is not None) # Get the context menu items context_menu_items = self.__get_context_menu_items( self.channelObject, item=media_item) kodi_item.addContextMenuItems(context_menu_items) # Get the action URL url = media_item.actionUrl if url is None: url = self._create_action_url(self.channelObject, action=action, item=media_item, store_id=parent_guid) # Add them to the list of Kodi items kodi_items.append((url, kodi_item, folder)) watcher.lap("Kodi Items generated") # add items but if OK was False, keep it like that ok = ok and xbmcplugin.addDirectoryItems(self.handle, kodi_items, len(kodi_items)) watcher.lap("items send to Kodi") if ok and parent_guid is not None: self._pickler.store_media_items(parent_guid, selected_item, media_items) watcher.stop() self.__add_sort_method_to_handle(self.handle, media_items) self.__add_breadcrumb(self.handle, self.channelObject, selected_item) # set the content. It needs to be "episodes" to make the MediaItem.set_season_info() work xbmcplugin.setContent(handle=self.handle, content="episodes") xbmcplugin.endOfDirectory(self.handle, ok) except Exception: Logger.error("Plugin::Error Processing FolderList", exc_info=True) XbmcWrapper.show_notification( LanguageHelper.get_localized_string(LanguageHelper.ErrorId), LanguageHelper.get_localized_string(LanguageHelper.ErrorList), XbmcWrapper.Error, 4000) xbmcplugin.endOfDirectory(self.handle, False) # @LockWithDialog(logger=Logger.instance()) No longer needed as Kodi will do this automatically def play_video_item(self): """ Starts the videoitem using a playlist. """ from resources.lib import player Logger.debug("Playing videoitem using PlayListMethod") try: media_item = self.media_item if not media_item.complete: media_item = self.channelObject.process_video_item(media_item) # Any warning to show self.__show_warnings(media_item) # validated the updated media_item if not media_item.complete or not media_item.has_media_item_parts( ): Logger.warning( "process_video_item returned an MediaItem that had MediaItem.complete = False:\n%s", media_item) if not media_item.has_media_item_parts(): # the update failed or no items where found. Don't play XbmcWrapper.show_notification( LanguageHelper.get_localized_string( LanguageHelper.ErrorId), LanguageHelper.get_localized_string( LanguageHelper.NoStreamsId), XbmcWrapper.Error) Logger.warning( "Could not start playback due to missing streams. Item:\n%s", media_item) xbmcplugin.endOfDirectory(self.handle, False) return kodi_items = media_item.get_kodi_play_list_data( AddonSettings.get_max_stream_bitrate(self.channelObject), self.channelObject.proxy) Logger.debug("Continuing playback in plugin.py") if not bool(kodi_items): Logger.warning("play_video_item did not return valid playdata") xbmcplugin.endOfDirectory(self.handle, False) return # Now we force the busy dialog to close, else the video will not play and the # setResolved will not work. LockWithDialog.close_busy_dialog() # Append it to the Kodi playlist in a smart way. start_url = self.__append_kodi_play_list(kodi_items) # Set the mode (if the InputStream Adaptive add-on is used, we also need to set it) show_subs = AddonSettings.show_subtitles() # TODO: Apparently if we use the InputStream Adaptive, using the setSubtitles() causes sync issues. available_subs = [p.Subtitle for p in media_item.MediaItemParts] # Get the Kodi Player instance (let Kodi decide what player, see # http://forum.kodi.tv/showthread.php?tid=173887&pid=1516662#pid1516662) kodi_player = player.Player(show_subs=show_subs, subs=available_subs) kodi_player.waitForPlayBack(url=start_url, time_out=10) xbmcplugin.endOfDirectory(self.handle, True) except: XbmcWrapper.show_notification( LanguageHelper.get_localized_string(LanguageHelper.ErrorId), LanguageHelper.get_localized_string( LanguageHelper.NoPlaybackId), XbmcWrapper.Error) Logger.critical("Could not playback the url", exc_info=True) # We need to single Kodi that it failed and it should not wait longer. Either using a # `raise` or with `xbmcplugin.endOfDirectory`. Doing the latter for now although we are # not really playing. xbmcplugin.endOfDirectory(self.handle, False) return def on_action_from_context_menu(self, action): """Peforms the action from a custom contextmenu Arguments: action : String - The name of the method to call """ Logger.debug("Performing Custom Contextmenu command: %s", action) item = self.media_item if not item.complete: Logger.debug( "The contextmenu action requires a completed item. Updating %s", item) item = self.channelObject.process_video_item(item) if not item.complete: Logger.warning( "update_video_item returned an item that had item.complete = False:\n%s", item) # invoke function_string = "returnItem = self.channelObject.%s(item)" % ( action, ) Logger.debug("Calling '%s'", function_string) try: # noinspection PyRedundantParentheses exec(function_string) # NOSONAR We just need this here. except: Logger.error("on_action_from_context_menu :: Cannot execute '%s'.", function_string, exc_info=True) return def __fetch_textures(self): textures_to_retrieve = TextureHandler.instance( ).number_of_missing_textures() if textures_to_retrieve > 0: w = None try: # show a blocking or background progress bar if textures_to_retrieve > 4: w = XbmcDialogProgressWrapper( "%s: %s" % (Config.appName, LanguageHelper.get_localized_string( LanguageHelper.InitChannelTitle)), LanguageHelper.get_localized_string( LanguageHelper.FetchTexturesTitle), # Config.textureUrl ) else: w = XbmcDialogProgressBgWrapper( "%s: %s" % (Config.appName, LanguageHelper.get_localized_string( LanguageHelper.FetchTexturesTitle)), Config.textureUrl) TextureHandler.instance().fetch_textures(w.progress_update) except: Logger.error("Error fetching textures", exc_info=True) finally: if w is not None: # always close the progress bar w.close() return def __configure_channel(self, channel_info): """ Shows the current channels settings dialog. :param ChannelInfo channel_info: The channel info for the channel """ if not channel_info: Logger.warning("Cannot configure channel without channel info") Logger.info("Configuring channel: %s", channel_info) AddonSettings.show_channel_settings(channel_info) def __add_sort_method_to_handle(self, handle, items=None): """ Add a sort method to the plugin output. It takes the Add-On settings into account. But if none of the items have a date, it is forced to sort by name. :param int handle: The handle to add the sortmethod to. :param list[MediaItem] items: The items that need to be sorted :rtype: None """ if AddonSettings.mix_folders_and_videos(): label_sort_method = xbmcplugin.SORT_METHOD_LABEL_IGNORE_FOLDERS else: label_sort_method = xbmcplugin.SORT_METHOD_LABEL if items: has_dates = len(list([i for i in items if i.has_date()])) > 0 if has_dates: Logger.debug("Sorting method: Dates") xbmcplugin.addSortMethod( handle=handle, sortMethod=xbmcplugin.SORT_METHOD_DATE) xbmcplugin.addSortMethod(handle=handle, sortMethod=label_sort_method) xbmcplugin.addSortMethod( handle=handle, sortMethod=xbmcplugin.SORT_METHOD_TRACKNUM) xbmcplugin.addSortMethod( handle=handle, sortMethod=xbmcplugin.SORT_METHOD_UNSORTED) return has_tracks = len(list([i for i in items if i.has_track()])) > 0 if has_tracks: Logger.debug("Sorting method: Tracks") xbmcplugin.addSortMethod( handle=handle, sortMethod=xbmcplugin.SORT_METHOD_TRACKNUM) xbmcplugin.addSortMethod( handle=handle, sortMethod=xbmcplugin.SORT_METHOD_DATE) xbmcplugin.addSortMethod(handle=handle, sortMethod=label_sort_method) xbmcplugin.addSortMethod( handle=handle, sortMethod=xbmcplugin.SORT_METHOD_UNSORTED) return Logger.debug("Sorting method: Default (Label)") xbmcplugin.addSortMethod(handle=handle, sortMethod=label_sort_method) xbmcplugin.addSortMethod(handle=handle, sortMethod=xbmcplugin.SORT_METHOD_DATE) xbmcplugin.addSortMethod(handle=handle, sortMethod=xbmcplugin.SORT_METHOD_TRACKNUM) xbmcplugin.addSortMethod(handle=handle, sortMethod=xbmcplugin.SORT_METHOD_UNSORTED) return def __get_context_menu_items(self, channel, item=None): """ Retrieves the custom context menu items to display. favouritesList : Boolean - Indication that the menu is for the favorites :param Channel|None channel: The channel from which to get the context menu items. The channel might be None in case of some actions that do not require a channel. :param MediaItem|None item: The item to which the context menu belongs. :return: A list of context menu names and their commands. :rtype: list[tuple[str,str]] """ context_menu_items = [] # Genenric, none-Python menu items that would normally cause an unwanted reload of the # Python interpreter instance within Kodi. refresh = LanguageHelper.get_localized_string( LanguageHelper.RefreshListId) context_menu_items.append((refresh, 'XBMC.Container.Refresh()')) if item is None: return context_menu_items # if it was a favourites list, don't add the channel methods as they might be from a different channel if channel is None: return context_menu_items if item.has_info(): info_action = LanguageHelper.get_localized_string( LanguageHelper.ItemInfo) context_menu_items.append((info_action, 'Action(info)')) # now we process the other items possible_methods = self.__get_members(channel) # Logger.Debug(possible_methods) for menu_item in channel.contextMenuItems: # Logger.Debug(menu_item) if menu_item.itemTypes is None or item.type in menu_item.itemTypes: # We don't care for complete here! # if menu_item.completeStatus == None or menu_item.completeStatus == item.complete: # see if the method is available method_available = False for method in possible_methods: if method == menu_item.functionName: method_available = True # break from the method loop break if not method_available: Logger.warning("No method for: %s", menu_item) continue cmd_url = self._create_action_url( channel, action=menu_item.functionName, item=item) cmd = "XBMC.RunPlugin(%s)" % (cmd_url, ) title = "Retro: %s" % (menu_item.label, ) Logger.trace("Adding command: %s | %s", title, cmd) context_menu_items.append((title, cmd)) return context_menu_items def __get_members(self, channel): """ Caches the inspect.getmembers(channel) or dir(channel) method for performance matters. :param Channel channel: The channel from which to get the context menu items. The channel might be None in case of some actions that do not require a channel. :return: A list of all methods in the channel. :rtype: list[str] """ if channel.guid not in self.methodContainer: # Not working on all platforms # self.methodContainer[channel.guid] = inspect.getmembers(channel) self.methodContainer[channel.guid] = dir(channel) return self.methodContainer[channel.guid] def __show_empty_information(self, items, favs=False): """ Adds an empty item to a list or just shows a message. @type favs: boolean @param items: :param list[MediaItem] items: The list of items. :param bool favs: Indicating that we are dealing with favourites. :return: boolean indicating to report the listing as succes or not. :rtype: ok """ if favs: title = LanguageHelper.get_localized_string( LanguageHelper.NoFavsId) else: title = LanguageHelper.get_localized_string( LanguageHelper.ErrorNoEpisodes) behaviour = AddonSettings.get_empty_list_behaviour() Logger.debug("Showing empty info for mode (favs=%s): [%s]", favs, behaviour) if behaviour == "error": # show error ok = False elif behaviour == "dummy" and not favs: # We should add a dummy items, but not for favs empty_list_item = MediaItem("- %s -" % (title.strip("."), ), "", type='video') empty_list_item.dontGroup = True empty_list_item.complete = True # add funny stream here? # part = empty_list_item.create_new_empty_media_part() # for s, b in YouTube.get_streams_from_you_tube("", self.channelObject.proxy): # part.append_media_stream(s, b) # if we add one, set OK to True ok = True items.append(empty_list_item) else: ok = True XbmcWrapper.show_notification( LanguageHelper.get_localized_string(LanguageHelper.ErrorId), title, XbmcWrapper.Error, 2500) return ok @LockWithDialog(logger=Logger.instance()) def __send_log(self): """ Send log files via Pastbin or Gist. """ from resources.lib.helpers.logsender import LogSender sender_mode = 'hastebin' log_sender = LogSender(Config.logSenderApi, logger=Logger.instance(), mode=sender_mode) try: title = LanguageHelper.get_localized_string( LanguageHelper.LogPostSuccessTitle) url_text = LanguageHelper.get_localized_string( LanguageHelper.LogPostLogUrl) files_to_send = [ Logger.instance().logFileName, Logger.instance().logFileName.replace(".log", ".old.log") ] if sender_mode != "gist": paste_url = log_sender.send_file(Config.logFileNameAddon, files_to_send[0]) else: paste_url = log_sender.send_files(Config.logFileNameAddon, files_to_send) XbmcWrapper.show_dialog(title, url_text % (paste_url, )) except Exception as e: Logger.error("Error sending %s", Config.logFileNameAddon, exc_info=True) title = LanguageHelper.get_localized_string( LanguageHelper.LogPostErrorTitle) error_text = LanguageHelper.get_localized_string( LanguageHelper.LogPostError) error = error_text % (str(e), ) XbmcWrapper.show_dialog(title, error.strip(": ")) @LockWithDialog(logger=Logger.instance()) def __set_proxy(self, language, proxy_id, local_ip): """ Sets the proxy and local IP configuration for channels. :param str language: The language for what channels to update. :param int proxy_id: The proxy index to use. :param int local_ip: The local_ip index to use. If no proxy_id is specified (None) then the proxy_id will be determined based on language If no local_ip is specified (None) then the local_ip will be determined based on language """ languages = AddonSettings.get_available_countries( as_country_codes=True) if language is not None and language not in languages: Logger.warning("Missing language: %s", language) return if proxy_id is None: proxy_id = languages.index(language) else: # noinspection PyTypeChecker proxy_id = int(proxy_id) if local_ip is None: local_ip = languages.index(language) else: # noinspection PyTypeChecker local_ip = int(local_ip) channels = ChannelIndex.get_register().get_channels() Logger.info( "Setting proxy='%s' (%s) and local_ip='%s' (%s) for country '%s'", proxy_id, languages[proxy_id], local_ip, languages[local_ip], language) channels_in_country = [ c for c in channels if c.language == language or language is None ] for channel in channels_in_country: Logger.debug("Setting Proxy for: %s", channel) AddonSettings.set_proxy_id_for_channel(channel, proxy_id) if channel.localIPSupported: Logger.debug("Setting Local IP for: %s", channel) AddonSettings.set_local_ip_for_channel(channel, local_ip) def __set_kodi_properties(self, kodi_item, media_item, is_folder, is_favourite): """ Sets any Kodi related properties. :param xbmcgui.ListItem kodi_item: The Kodi list item. :param MediaItem media_item: The internal media item. :param bool is_folder: Is this a folder. :param bool is_favourite: Is this a favourite. """ # Set the properties for the context menu add-on kodi_item.setProperty(self.propertyRetrospect, "true") kodi_item.setProperty( self.propertyRetrospectFolder if is_folder else self.propertyRetrospectVideo, "true") if is_favourite: kodi_item.setProperty(self.propertyRetrospectFavorite, "true") elif media_item.isCloaked: kodi_item.setProperty(self.propertyRetrospectCloaked, "true") if self.channelObject and self.channelObject.adaptiveAddonSelectable: kodi_item.setProperty(self.propertyRetrospectAdaptive, "true") if self.channelObject and self.channelObject.hasSettings: kodi_item.setProperty(self.propertyRetrospectChannelSetting, "true") def __show_warnings(self, media_item): """ Show playback warnings for this MediaItem :param MediaItem media_item: The current MediaItem that will be played. """ if (media_item.isDrmProtected or media_item.isPaid) and AddonSettings.show_drm_paid_warning(): if media_item.isDrmProtected: Logger.debug("Showing DRM Warning message") title = LanguageHelper.get_localized_string( LanguageHelper.DrmTitle) message = LanguageHelper.get_localized_string( LanguageHelper.DrmText) XbmcWrapper.show_dialog(title, message) elif media_item.isPaid: Logger.debug("Showing Paid Warning message") title = LanguageHelper.get_localized_string( LanguageHelper.PaidTitle) message = LanguageHelper.get_localized_string( LanguageHelper.PaidText) XbmcWrapper.show_dialog(title, message) # noinspection PyUnusedLocal def __add_breadcrumb(self, handle, channel, selected_item, last_only=False): """ Updates the Kodi category with a breadcrumb to the current parent item :param int handle: The Kodi file handle :param ChannelInfo|Channel channel: The channel to which the item belongs :param MediaItem selected_item: The item from which to show the breadcrumbs :param bool last_only: Show only the last item """ bread_crumb = None if selected_item is not None: bread_crumb = selected_item.name elif self.channelObject is not None: bread_crumb = channel.channelName if not bread_crumb: return bread_crumb = HtmlEntityHelper.convert_html_entities(bread_crumb) xbmcplugin.setPluginCategory(handle=handle, category=bread_crumb) def __append_kodi_play_list(self, kodi_items): # Get the current playlist play_list = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) play_list_size = play_list.size() start_index = play_list.getposition() # the current location if start_index < 0: start_index = 0 Logger.debug("Current playlist size and position: %s @ %s", play_list_size, start_index) current_play_list_items = [ play_list[i] for i in range(0, len(play_list)) ] play_list.clear() for i in range(0, start_index): Logger.debug("Adding existing PlayList item") play_list.add(current_play_list_items[i].getfilename(), current_play_list_items[i]) # The current item to play (we need to store te starting url for later) Logger.debug("Adding Main PlayList item") kodi_item, start_url = kodi_items.pop(0) play_list.add(start_url, kodi_item, start_index) xbmcplugin.setResolvedUrl(self.handle, True, kodi_item) # now we add the rest of the Kodi ListItems for the other parts for kodi_item, stream_url in kodi_items: Logger.debug("Adding Additional PlayList item") play_list.add(stream_url, kodi_item, start_index) xbmcplugin.setResolvedUrl(self.handle, True, kodi_item) # add the remaining items for i in range(start_index + 1, len(current_play_list_items)): Logger.debug("Adding existing PlayList item") play_list.add(current_play_list_items[i].getfilename(), current_play_list_items[i]) return start_url def __update_artwork(self, media_item, channel): """ Updates the fanart and icon of a MediaItem if thoses are missing. :param MediaItem media_item: The item to update :param Channel channel: A possible selected channel """ if media_item is None: return if channel: # take the channel values fallback_icon = self.channelObject.icon fallback_thumb = self.channelObject.noImage fallback_fanart = self.channelObject.fanart parent_item = channel.parentItem else: # else the Retrospect ones fallback_icon = Config.icon fallback_thumb = Config.fanart fallback_fanart = Config.fanart parent_item = None if parent_item is not None: fallback_thumb = parent_item.thumb or fallback_thumb fallback_fanart = parent_item.fanart or fallback_fanart # keep it or use the fallback media_item.icon = media_item.icon or fallback_icon media_item.thumb = media_item.thumb or fallback_thumb media_item.fanart = media_item.fanart or fallback_fanart if AddonSettings.use_thumbs_as_fanart() and \ TextureHandler.instance().is_texture_or_empty(media_item.fanart) and \ not TextureHandler.instance().is_texture_or_empty(media_item.thumb): media_item.fanart = media_item.thumb return
class Menu(ParameterParser): def __init__(self, action): Logger.info( "**** Starting menu '%s' for %s add-on version %s/repo ****", action, Config.appName, Config.version) # noinspection PyUnresolvedReferences self.kodiItem = sys.listitem params = self.kodiItem.getPath() if not params: self.channelObject = None return name, params = params.split("?", 1) params = "?{0}".format(params) # Main constructor parses super(Menu, self).__init__(name, params) self.channelObject = self.__get_channel() Logger.debug( "Plugin Params: %s (%s)\n" "Name: %s\n" "Query: %s", self.params, len(self.params), self.pluginName, params) if self.keywordPickle in self.params: self.mediaItem = self._pickler.de_pickle_media_item( self.params[self.keywordPickle]) else: self.mediaItem = None def hide_channel(self): """ Hides a specific channel """ Logger.info("Hiding channel: %s", self.channelObject) AddonSettings.set_channel_visiblity(self.channelObject, False) self.refresh() def select_channels(self): """ Selects the channels that should be visible. @return: None """ valid_channels = ChannelIndex.get_register().get_channels( include_disabled=True) channels_to_show = [c for c in valid_channels if c.visible] # The old way # channels_to_show = filter(lambda c: c.visible, valid_channels) selected_channels = [c for c in channels_to_show if c.enabled] selected_indices = list( [channels_to_show.index(c) for c in selected_channels]) Logger.debug("Currently selected channels: %s", selected_indices) channel_to_show_names = [ HtmlEntityHelper.convert_html_entities(c.channelName) for c in channels_to_show ] # The old way # channel_to_show_names = list(map(lambda c: HtmlEntityHelper.convert_html_entities(c.channelName), channels_to_show)) dialog = xbmcgui.Dialog() heading = LanguageHelper.get_localized_string( LanguageHelper.ChannelSelection)[:-1] selected_channels = dialog.multiselect(heading, channel_to_show_names, preselect=selected_indices) if selected_channels is None: return selected_channels = list(selected_channels) Logger.debug("New selected channels: %s", selected_channels) indices_to_remove = [ i for i in selected_indices if i not in selected_channels ] indices_to_add = [ i for i in selected_channels if i not in selected_indices ] for i in indices_to_remove: Logger.info("Hiding channel: %s", channels_to_show[i]) AddonSettings.set_channel_visiblity(channels_to_show[i], False) for i in indices_to_add: Logger.info("Showing channel: %s", channels_to_show[i]) AddonSettings.set_channel_visiblity(channels_to_show[i], True) self.refresh() return def show_country_settings(self): """ Shows the country settings page where channels can be shown/hidden based on the country of origin. """ if AddonSettings.is_min_version(18): AddonSettings.show_settings(-99) else: AddonSettings.show_settings(101) self.refresh() def show_settings(self): """ Shows the add-on settings page and refreshes when closing it. """ AddonSettings.show_settings() self.refresh() def channel_settings(self): """ Shows the channel settings for the selected channel. Refreshes the list after closing the settings. """ AddonSettings.show_channel_settings(self.channelObject) self.refresh() def favourites(self, all_favorites=False): """ Shows the favourites, either for a channel or all that are known. @param all_favorites: if True the list will return all favorites. Otherwise it will only only return the channel ones. """ # it's just the channel, so only add the favourites cmd_url = self._create_action_url( None if all_favorites else self.channelObject, action=self.actionAllFavourites if all_favorites else self.actionFavourites) xbmc.executebuiltin("XBMC.Container.Update({0})".format(cmd_url)) @LockWithDialog(logger=Logger.instance()) def add_favourite(self): """ Adds the selected item to the favourites. The opens the favourite list. """ # remove the item item = self._pickler.de_pickle_media_item( self.params[self.keywordPickle]) # no need for dates in the favourites # item.clear_date() Logger.debug("Adding favourite: %s", item) f = Favourites(Config.favouriteDir) if item.is_playable(): action = self.actionPlayVideo else: action = self.actionListFolder # add the favourite f.add(self.channelObject, item, self._create_action_url(self.channelObject, action, item)) # we are finished, so just open the Favorites self.favourites() @LockWithDialog(logger=Logger.instance()) def remove_favourite(self): """ Remove the selected favourite and then refresh the favourite list. """ # remove the item item = self._pickler.de_pickle_media_item( self.params[self.keywordPickle]) Logger.debug("Removing favourite: %s", item) f = Favourites(Config.favouriteDir) f.remove(item) # refresh the list self.refresh() def refresh(self): """ Refreshes the current Kodi list """ xbmc.executebuiltin("XBMC.Container.Refresh()") def toggle_cloak(self): """ Toggles the cloaking (showing/hiding) of the selected folder. """ item = self._pickler.de_pickle_media_item( self.params[self.keywordPickle]) Logger.info("Cloaking current item: %s", item) c = Cloaker(self.channelObject, AddonSettings.store(LOCAL), logger=Logger.instance()) if c.is_cloaked(item.url): c.un_cloak(item.url) self.refresh() return first_time = c.cloak(item.url) if first_time: XbmcWrapper.show_dialog( LanguageHelper.get_localized_string( LanguageHelper.CloakFirstTime), LanguageHelper.get_localized_string( LanguageHelper.CloakMessage)) del c self.refresh() def set_bitrate(self): """ Sets the bitrate for the selected channel via a specific dialog. """ if self.channelObject is None: raise ValueError("Missing channel") # taken from the settings.xml bitrate_options = "Retrospect|100|250|500|750|1000|1500|2000|2500|4000|8000|20000"\ .split("|") current_bitrate = AddonSettings.get_max_channel_bitrate( self.channelObject) Logger.debug("Found bitrate for %s: %s", self.channelObject, current_bitrate) current_bitrate_index = 0 if current_bitrate not in bitrate_options \ else bitrate_options.index(current_bitrate) dialog = xbmcgui.Dialog() heading = LanguageHelper.get_localized_string( LanguageHelper.BitrateSelection) selected_bitrate = dialog.select(heading, bitrate_options, preselect=current_bitrate_index) if selected_bitrate < 0: return Logger.info("Changing bitrate for %s from %s to %s", self.channelObject, bitrate_options[current_bitrate_index], bitrate_options[selected_bitrate]) AddonSettings.set_max_channel_bitrate( self.channelObject, bitrate_options[selected_bitrate]) return def set_inputstream_adaptive(self): """ Set the InputStream Adaptive for this channel """ if self.channelObject is None: raise ValueError("Missing channel") if not self.channelObject.adaptiveAddonSelectable: Logger.warning( "Cannot set InputStream Adaptive add-on mode for %s", self.channelObject) return current_mode = AddonSettings.get_adaptive_mode(self.channelObject) mode_values = [None, True, False] current_index = mode_values.index(current_mode) mode_options = [ LanguageHelper.get_localized_string(LanguageHelper.Retrospect), LanguageHelper.get_localized_string(LanguageHelper.Enabled), LanguageHelper.get_localized_string(LanguageHelper.Disabled) ] dialog = xbmcgui.Dialog() heading = LanguageHelper.get_localized_string( LanguageHelper.ChannelAdaptiveMode) selected_index = dialog.select(heading, mode_options, preselect=current_index) if selected_index < 0: return selected_value = mode_values[selected_index] Logger.info("Changing InputStream Adaptive mode for %s from %s to %s", self.channelObject, mode_options[current_index], mode_options[selected_index]) AddonSettings.set_adaptive_mode(self.channelObject, selected_value) # Refresh if we have a video item selected, so the cached urls are removed. if self.keywordPickle in self.params: Logger.debug("Refreshing list to clear URL caches") self.refresh() def __get_channel(self): chn = self.params.get(self.keywordChannel, None) code = self.params.get(self.keywordChannelCode, None) if not chn: return None Logger.debug("Fetching channel %s - %s", chn, code) channel = ChannelIndex.get_register().get_channel(chn, code, info_only=True) Logger.debug("Created channel: %s", channel) return channel def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): if exc_val: Logger.critical("Error in menu handling: %s", exc_val.message, exc_info=True) # make sure we leave no references behind AddonSettings.clear_cached_addon_settings_object() # close the log to prevent locking on next call Logger.instance().close_log() return False
def execute(self): Logger.info("Plugin::process_folder_list Doing process_folder_list") try: ok = True # read the item from the parameters selected_item = self.__media_item # determine the parent guid parent_guid = self.parameter_parser.get_parent_guid( self.__channel, selected_item) if self.__favorites is None: watcher = StopWatch("Plugin process_folder_list", Logger.instance()) media_items = self.__channel.process_folder_list(selected_item) watcher.lap("Class process_folder_list finished") else: parent_guid = "{}.fav".format(parent_guid) watcher = StopWatch("Plugin process_folder_list With Items", Logger.instance()) media_items = self.__favorites if len(media_items) == 0: Logger.warning("process_folder_list returned %s items", len(media_items)) ok = self.__show_empty_information(media_items, favs=self.__favorites is not None) else: Logger.debug("process_folder_list returned %s items", len(media_items)) kodi_items = [] use_thumbs_as_fanart = AddonSettings.use_thumbs_as_fanart() for media_item in media_items: # type: MediaItem self.__update_artwork(media_item, self.__channel, use_thumbs_as_fanart) if media_item.type == 'folder' or media_item.type == 'append' or media_item.type == "page": action_value = action.LIST_FOLDER folder = True elif media_item.is_playable(): action_value = action.PLAY_VIDEO folder = False else: Logger.critical( "Plugin::process_folder_list: Cannot determine what to add" ) continue # Get the Kodi item kodi_item = media_item.get_kodi_item() self.__set_kodi_properties(kodi_item, media_item, folder, is_favourite=self.__favorites is not None) # Get the context menu items context_menu_items = self._get_context_menu_items( self.__channel, item=media_item) kodi_item.addContextMenuItems(context_menu_items) # Get the action URL url = media_item.actionUrl if url is None: url = self.parameter_parser.create_action_url( self.__channel, action=action_value, item=media_item, store_id=parent_guid) # Add them to the list of Kodi items kodi_items.append((url, kodi_item, folder)) watcher.lap("Kodi Items generated") # add items but if OK was False, keep it like that ok = ok and xbmcplugin.addDirectoryItems(self.handle, kodi_items, len(kodi_items)) watcher.lap("items send to Kodi") if ok and parent_guid is not None: self.parameter_parser.pickler.store_media_items( parent_guid, selected_item, media_items) watcher.stop() self.__add_sort_method_to_handle(self.handle, media_items) self.__add_breadcrumb(self.handle, self.__channel, selected_item) self.__add_content_type(self.handle, self.__channel, selected_item) xbmcplugin.endOfDirectory(self.handle, ok) except Exception: Logger.error("Plugin::Error Processing FolderList", exc_info=True) XbmcWrapper.show_notification( LanguageHelper.get_localized_string(LanguageHelper.ErrorId), LanguageHelper.get_localized_string(LanguageHelper.ErrorList), XbmcWrapper.Error, 4000) xbmcplugin.endOfDirectory(self.handle, False)
from resources.lib.logger import Logger Logger.create_logger(os.path.join(Config.profileDir, Config.logFileNameAddon), Config.appName, append=True, dual_logger=lambda x, y=4: xbmc.log(x, y)) from resources.lib.helpers.htmlentityhelper import HtmlEntityHelper from resources.lib.addonsettings import AddonSettings, LOCAL from resources.lib.favourites import Favourites from resources.lib.paramparser import ParameterParser from resources.lib.helpers.channelimporter import ChannelIndex from resources.lib.helpers.languagehelper import LanguageHelper from resources.lib.locker import LockWithDialog from resources.lib.cloaker import Cloaker from resources.lib.xbmcwrapper import XbmcWrapper Logger.instance().minLogLevel = AddonSettings.get_log_level() class Menu(ParameterParser): def __init__(self, action): Logger.info( "**** Starting menu '%s' for %s add-on version %s/repo ****", action, Config.appName, Config.version) # noinspection PyUnresolvedReferences self.kodiItem = sys.listitem params = self.kodiItem.getPath() if not params: self.channelObject = None return
def update_video_item(self, item): """ Updates an existing MediaItem with more data. Used to update none complete MediaItems (self.complete = False). This could include opening the item's URL to fetch more data and then process that data or retrieve it's real media-URL. The method should at least: * cache the thumbnail to disk (use self.noImage if no thumb is available). * set at least one MediaItemPart with a single MediaStream. * set self.complete = True. if the returned item does not have a MediaItemPart then the self.complete flag will automatically be set back to False. :param MediaItem item: the original MediaItem that needs updating. :return: The original item with more data added to it's properties. :rtype: MediaItem """ Logger.debug('Starting update_video_item for %s (%s)', item.name, self.channelName) data = UriHandler.open(item.url, proxy=self.proxy) json = JsonHelper(data, Logger.instance()) video_data = json.get_value("video") if video_data: part = item.create_new_empty_media_part() if self.localIP: part.HttpHeaders.update(self.localIP) # get the videos video_urls = video_data.get("videoReferences") for video_url in video_urls: # Logger.Trace(videoUrl) stream_info = video_url['url'] if "manifest.f4m" in stream_info: continue elif "master.m3u8" in stream_info: for s, b in M3u8.get_streams_from_m3u8(stream_info, self.proxy, headers=part.HttpHeaders): item.complete = True part.append_media_stream(s, b) # subtitles subtitles = video_data.get("subtitleReferences") if subtitles and subtitles[0]["url"]: Logger.trace(subtitles) sub_url = subtitles[0]["url"] file_name = "%s.srt" % (EncodingHelper.encode_md5(sub_url),) sub_data = UriHandler.open(sub_url, proxy=self.proxy) # correct the subs regex = re.compile(r"^1(\d:)", re.MULTILINE) sub_data = re.sub(regex, r"0\g<1>", sub_data) sub_data = re.sub(r"--> 1(\d):", r"--> 0\g<1>:", sub_data) local_complete_path = os.path.join(Config.cacheDir, file_name) Logger.debug("Saving subtitle to: %s", local_complete_path) with open(local_complete_path, 'w') as f: f.write(sub_data) part.Subtitle = local_complete_path item.complete = True return item
def get_channels(self, include_disabled=False, **kwargs): # NOSONAR """ Retrieves all enabled channels within Retrospect. If updated channels are found, the those channels are indexed and the channel index is rebuild. :param bool include_disabled: Boolean to indicate if we should include those channels that are explicitly disabled from the settings. :param dict kwargs: Here for backward compatibility. :return: a list of ChannelInfo objects of enabled channels. :rtype: list[ChannelInfo] """ sw = StopWatch("ChannelIndex.get_channels Importer", Logger.instance()) Logger.info("Fetching all enabled channels.") self.__allChannels = [] valid_channels = [] channels_updated = False country_visibility = {} channel_path = os.path.join(Config.rootDir, self.__INTERNAL_CHANNEL_PATH) for channel_pack in os.listdir(channel_path): if not channel_pack.startswith("channel."): continue for channel_set in os.listdir(os.path.join(channel_path, channel_pack)): channel_set_path = os.path.join(channel_path, channel_pack, channel_set) if not os.path.isdir(channel_set_path): continue channel_set_info_path = os.path.join(channel_set_path, "chn_{}.json".format(channel_set)) channel_infos = ChannelInfo.from_json(channel_set_info_path) # Check if the channel was updated if self.__is_channel_set_updated(channel_infos[0]): if not channels_updated: # this was the first update found (otherwise channelsUpdated was True) show a message: title = LanguageHelper.get_localized_string(LanguageHelper.InitChannelTitle) text = LanguageHelper.get_localized_string(LanguageHelper.InitChannelText) XbmcWrapper.show_notification(title, text, display_time=15000, logger=Logger.instance()) channels_updated |= True # Initialise the channelset. self.__initialise_channel_set(channel_infos[0]) # And perform all first actions for the included channels in the set for channel_info in channel_infos: self.__initialise_channel(channel_info) # Check the channel validity for channel_info in channel_infos: if not self.__channel_is_correct(channel_info): continue self.__allChannels.append(channel_info) if channel_info.ignore: Logger.warning("Not loading: %s -> ignored in the channel set", channel_info) continue valid_channels.append(channel_info) # was the channel hidden based on language settings? We do some caching to speed # things up. if channel_info.language not in country_visibility: country_visibility[channel_info.language] = AddonSettings.show_channel_with_language(channel_info.language) channel_info.visible = country_visibility[channel_info.language] # was the channel explicitly disabled from the settings? channel_info.enabled = AddonSettings.get_channel_visibility(channel_info) Logger.debug("Found channel: %s", channel_info) if channels_updated: Logger.info("New or updated channels found. Updating add-on configuration for all channels and user agent.") AddonSettings.update_add_on_settings_with_channels(valid_channels, Config) AddonSettings.update_user_agent() else: Logger.debug("No channel changes found. Skipping add-on configuration for channels.") # TODO: perhaps we should check that the settings.xml is correct and not broken? valid_channels.sort(key=lambda c: c.sort_key) visible_channels = [ci for ci in valid_channels if ci.visible and ci.enabled] Logger.info("Fetch a total of %d channels of which %d are visible.", len(valid_channels), len(visible_channels)) sw.stop() if include_disabled: return valid_channels return visible_channels
def process_folder_list(self, item=None): # NOSONAR """ Process the selected item and get's it's child items using the available dataparsers. Accepts an <item> and returns a list of MediaListems with at least name & url set. The following actions are done: * determining the correct parsers to use * call a pre-processor * parsing the data with the parsers * calling the creators for item creations if the item is None, we assume that we are dealing with the first call for this channel and the mainlist uri is used. :param MediaItem|None item: The parent item. :return: A list of MediaItems that form the childeren of the <item>. :rtype: list[MediaItem] """ items = [] self.parentItem = item if item is None: Logger.info( "process_folder_list :: No item was specified. Assuming it was the main channel list" ) url = self.mainListUri elif len(item.items) > 0: return item.items else: url = item.url # Determine the handlers and process data_parsers = self.__get_data_parsers(url) # Exclude the updaters only data_parsers = [ p for p in data_parsers if not p.is_video_updater_only() ] if [p for p in data_parsers if p.LogOnRequired]: Logger.info("One or more dataparsers require logging in.") self.loggedOn = self.log_on() # now set the headers here and not earlier in case they might have been update by the logon if item is not None and item.HttpHeaders: headers = item.HttpHeaders else: headers = self.httpHeaders # Let's retrieve the required data. Main url's if url.startswith("http:") or url.startswith( "https:") or url.startswith("file:"): # Disable cache on live folders no_cache = item is not None and not item.is_playable( ) and item.isLive if no_cache: Logger.debug("Disabling cache for '%s'", item) data = UriHandler.open(url, proxy=self.proxy, additional_headers=headers, no_cache=no_cache) # Searching a site using search_site() elif url == "searchSite" or url == "#searchSite": Logger.debug("Starting to search") return self.search_site() # Labels instead of url's elif url.startswith("#"): data = "" # Others else: Logger.debug("Unknown URL format. Setting data to ''") data = "" # first check if there is a generic pre-processor pre_procs = [p for p in data_parsers if p.is_generic_pre_processor()] num_pre_procs = len(pre_procs) Logger.trace("Processing %s Generic Pre-Processors DataParsers", num_pre_procs) if num_pre_procs > 1: # warn for strange results if more than 1 generic pre-processor is present. Logger.warning( "More than one Generic Pre-Processor is found (%s). They are being processed in the " "order that Python likes which might result in unexpected result.", num_pre_procs) for data_parser in pre_procs: # remove it from the list data_parsers.remove(data_parser) # and process it Logger.debug("Processing %s", data_parser) (data, pre_items) = data_parser.PreProcessor(data) items += pre_items if isinstance(data, JsonHelper): Logger.debug( "Generic preprocessor resulted in JsonHelper data") # The the other handlers Logger.trace("Processing %s Normal DataParsers", len(data_parsers)) handler_json = None for data_parser in data_parsers: Logger.debug("Processing %s", data_parser) # Check for preprocessors if data_parser.PreProcessor: Logger.debug("Processing DataParser.PreProcessor") (handler_data, pre_items) = data_parser.PreProcessor(data) items += pre_items else: handler_data = data Logger.debug("Processing DataParser.Parser") if data_parser.Parser is None or (data_parser.Parser == "" and not data_parser.IsJson): if data_parser.Creator: Logger.warning("No <parser> found for %s. Skipping.", data_parser.Creator) continue if data_parser.IsJson: if handler_json is None: # Cache the json requests to improve performance Logger.trace("Caching JSON results for Dataparsing") if isinstance(handler_data, JsonHelper): handler_json = handler_data else: handler_json = JsonHelper(handler_data, Logger.instance()) Logger.trace(data_parser.Parser) parser_results = handler_json.get_value(fallback=[], *data_parser.Parser) if not isinstance(parser_results, (tuple, list)): # if there is just one match, return that as a list parser_results = [parser_results] else: if isinstance(handler_data, JsonHelper): raise ValueError( "Cannot perform Regex Parser on JsonHelper.") else: parser_results = Regexer.do_regex(data_parser.Parser, handler_data) Logger.debug("Processing DataParser.Creator for %s items", len(parser_results)) for parser_result in parser_results: handler_result = data_parser.Creator(parser_result) if handler_result is not None: if isinstance(handler_result, list): items += handler_result else: items.append(handler_result) # should we exclude DRM/GEO? hide_geo_locked = AddonSettings.hide_geo_locked_items_for_location( self.language) hide_drm_protected = AddonSettings.hide_drm_items() hide_premium = AddonSettings.hide_premium_items() hide_folders = AddonSettings.hide_restricted_folders() type_to_exclude = None if not hide_folders: type_to_exclude = "folder" old_count = len(items) if hide_drm_protected: Logger.debug("Hiding DRM items") items = [ i for i in items if not i.isDrmProtected or i.type == type_to_exclude ] if hide_geo_locked: Logger.debug("Hiding GEO Locked items due to GEO region: %s", self.language) items = [ i for i in items if not i.isGeoLocked or i.type == type_to_exclude ] if hide_premium: Logger.debug("Hiding Premium items") items = [ i for i in items if not i.isPaid or i.type == type_to_exclude ] # Local import for performance from resources.lib.cloaker import Cloaker cloaker = Cloaker(self, AddonSettings.store(LOCAL), logger=Logger.instance()) if not AddonSettings.show_cloaked_items(): Logger.debug("Hiding Cloaked items") items = [i for i in items if not cloaker.is_cloaked(i.url)] else: cloaked_items = [i for i in items if cloaker.is_cloaked(i.url)] for c in cloaked_items: c.isCloaked = True if len(items) != old_count: Logger.info( "Hidden %s items due to DRM/GEO/Premium/cloak filter (Hide Folders=%s)", old_count - len(items), hide_folders) # Check for grouping or not limit = AddonSettings.get_list_limit() folder_items = [i for i in items if i.type.lower() == "folder"] # we should also de-duplicate before calculating folder_items = list(set(folder_items)) folders = len(folder_items) if 0 < limit < folders: # let's filter them by alphabet if the number is exceeded Logger.debug( "Creating Groups for list exceeding '%s' folder items. Total folders found '%s'.", limit, folders) other = LanguageHelper.get_localized_string( LanguageHelper.OtherChars) title_format = LanguageHelper.get_localized_string( LanguageHelper.StartWith) result = dict() non_grouped = [] # Should we remove prefixes just as Kodi does? # prefixes = ("de", "het", "the", "een", "a", "an") for sub_item in items: if sub_item.dontGroup or sub_item.type != "folder": non_grouped.append(sub_item) continue char = sub_item.name[0].upper() # Should we de-prefix? # for p in prefixes: # if sub_item.name.lower().startswith(p + " "): # char = sub_item.name[len(p) + 1][0].upper() if char.isdigit(): char = "0-9" elif not char.isalpha(): char = other if char not in result: Logger.trace("Creating Grouped item from: %s", sub_item) if char == other: item = MediaItem( title_format.replace("'", "") % (char, ), "") else: item = MediaItem(title_format % (char.upper(), ), "") item.complete = True # item.set_date(2100 + ord(char[0]), 1, 1, text='') result[char] = item else: item = result[char] item.items.append(sub_item) items = non_grouped + list(result.values()) # In order to get a better performance in de-duplicating and keeping the sort order # we first need to store the order in a lookup table. Then we use sorted(set()) and # use that lookup table for sorting. Using sorted(set(), items.index) this will be # an O(n) (for the index()) times O(n*log(n)) (for the sorted) = O(n^2*log(n)!. # The dictionary lookup (O(1)) saves us an O(n). # See https://wiki.python.org/moin/TimeComplexity sorted_order = {} for i in range(0, len(items)): sorted_order[items[i]] = i unique_results = sorted(set(items), key=sorted_order.get) Logger.trace("Found '%d' items of which '%d' are unique.", len(items), len(unique_results)) return unique_results
def get_channels(self, include_disabled=False, **kwargs): # NOSONAR """ Retrieves all enabled channels within Retrospect. If updated channels are found, the those channels are indexed and the channel index is rebuild. :param bool include_disabled: Boolean to indicate if we should include those channels that are explicitly disabled from the settings. :param dict kwargs: Here for backward compatibility. :return: a list of ChannelInfo objects of enabled channels. :rtype: list[ChannelInfo] """ sw = StopWatch("ChannelIndex.get_channels Importer", Logger.instance()) Logger.info("Fetching all enabled channels.") self.__allChannels = [] valid_channels = [] # What platform are we platform = envcontroller.EnvController.get_platform() channels_updated = False country_visibility = {} for channel_set in self.__channelIndex[self.__CHANNEL_INDEX_CHANNEL_KEY]: channel_set = self.__channelIndex[self.__CHANNEL_INDEX_CHANNEL_KEY][channel_set] channel_set_info_path = channel_set[self.__CHANNEL_INDEX_CHANNEL_INFO_KEY] channel_set_version = channel_set[self.__CHANNEL_INDEX_CHANNEL_VERSION_KEY] # Check if file exists. If not, rebuild index if not os.path.isfile(channel_set_info_path) and not self.__reindexed: Logger.warning("Missing channelSet file: %s.", channel_set_info_path) self.__rebuild_index() return self.get_channels() channel_infos = ChannelInfo.from_json(channel_set_info_path, channel_set_version) # Check if the channel was updated if self.__is_channel_set_updated(channel_infos[0]): # let's see if the index has already been updated this section, of not, do it and # restart the ChannelRetrieval. if not self.__reindexed: # rebuild and restart Logger.warning("Re-index channel index due to channelSet update: %s.", channel_set_info_path) self.__rebuild_index() return self.get_channels() else: Logger.warning("Found updated channelSet: %s.", channel_set_info_path) if not channels_updated: # this was the first update found (otherwise channelsUpdated was True) show a message: title = LanguageHelper.get_localized_string(LanguageHelper.InitChannelTitle) text = LanguageHelper.get_localized_string(LanguageHelper.InitChannelText) XbmcWrapper.show_notification(title, text, display_time=15000, logger=Logger.instance()) channels_updated |= True # Initialise the channelset. self.__initialise_channel_set(channel_infos[0]) # And perform all first actions for the included channels in the set for channel_info in channel_infos: self.__initialise_channel(channel_info) # Check the channel validity for channel_info in channel_infos: if not self.__channel_is_correct(channel_info): continue self.__allChannels.append(channel_info) # valid channel for this platform ? if not channel_info.compatiblePlatforms & platform == platform: Logger.warning("Not loading: %s -> platform '%s' is not compatible.", channel_info, Environments.name(platform)) continue valid_channels.append(channel_info) # was the channel hidden based on language settings? We do some caching to speed # things up. if channel_info.language not in country_visibility: country_visibility[channel_info.language] = AddonSettings.show_channel_with_language(channel_info.language) channel_info.visible = country_visibility[channel_info.language] # was the channel explicitly disabled from the settings? channel_info.enabled = AddonSettings.get_channel_visibility(channel_info) Logger.debug("Found channel: %s", channel_info) if channels_updated: Logger.info("New or updated channels found. Updating add-on configuration for all channels and user agent.") AddonSettings.update_add_on_settings_with_channels(valid_channels, Config) AddonSettings.update_user_agent() else: Logger.debug("No channel changes found. Skipping add-on configuration for channels.") # TODO: perhaps we should check that the settings.xml is correct and not broken? valid_channels.sort(key=lambda c: c.sort_key) visible_channels = [ci for ci in valid_channels if ci.visible and ci.enabled] Logger.info("Fetch a total of %d channels of which %d are visible.", len(valid_channels), len(visible_channels)) sw.stop() if include_disabled: return valid_channels return visible_channels
def from_json(path): """ Generates a list of ChannelInfo objects present in the json meta data file. :param str path: The path of the json file. :return: The channel info objects within the json file. :rtype: list[ChannelInfo] """ if path in ChannelInfo.__channel_cache: Logger.debug( "Fetching ChannelInfo from ChannelInfo Cache for '%s'", path) return ChannelInfo.__channel_cache[path] channel_infos = [] with io.open(path, mode="r", encoding="utf-8") as json_file: json_data = json_file.read() json = JsonHelper(json_data, logger=Logger.instance()) channels = json.get_value("channels") # type: dict if "settings" in json.json: settings = json.get_value("settings") else: settings = [] Logger.debug("Found %s channels and %s settings in %s", len(channels), len(settings), path) for channel in channels: channel_info = ChannelInfo( channel["guid"], channel["name"], channel["description"], channel["icon"], channel["category"], path, # none required items channel.get("channelcode", None), channel.get("sortorder", 255), channel.get("language", None), channel.get("ignore", False), channel.get("fanart", None)) channel_info.firstTimeMessage = channel.get("message", None) channel_info.addonUrl = channel.get("addonUrl", None) channel_info.adaptiveAddonSelectable = channel.get( "adaptiveAddonSelectable", False) # Disable spoofing for the moment # channel_info.localIPSupported = channel.get("localIPSupported", False) channel_info.settings = settings # validate a bit if channel_info.channelCode == "None": raise Exception("'None' as channelCode") if channel_info.language == "None": raise Exception("'None' as language") channel_infos.append(channel_info) ChannelInfo.__channel_cache[path] = channel_infos return channel_infos
def pre_process_folder_list(self, data): """ Performs pre-process actions for data processing. Accepts an data from the process_folder_list method, BEFORE the items are processed. Allows setting of parameters (like title etc) for the channel. Inside this method the <data> could be changed and additional items can be created. The return values should always be instantiated in at least ("", []). :param str data: The retrieve data that was loaded for the current item and URL. :return: A tuple of the data and a list of MediaItems that were generated. :rtype: tuple[str|JsonHelper,list[MediaItem]] """ items = [] # We need to keep the JSON data, in order to refer to it from the create methods. self.currentJson = JsonHelper(data, Logger.instance()) # Extract season (called abstracts) information self.abstracts = dict() # : the season Logger.debug("Storing abstract information") for abstract in self.currentJson.get_value("abstracts"): self.abstracts[abstract["key"]] = abstract # If we have episodes available, list them self.episodes = dict() if "episodes" in self.currentJson.get_value(): Logger.debug("Storing episode information") for episode in self.currentJson.get_value("episodes"): self.episodes[episode["key"]] = episode # extract some meta data self.posterBase = self.currentJson.get_value("meta", "poster_base_url") self.thumbBase = self.currentJson.get_value("meta", "thumb_base_url") # And create page items items_on_page = int( self.currentJson.get_value("meta", "nr_of_videos_onpage")) total_items = int( self.currentJson.get_value("meta", "nr_of_videos_total")) current_page = self.currentJson.get_value("meta", "pg") if current_page == "all": current_page = 1 else: current_page = int(current_page) Logger.debug( "Found a total of %s items (%s items per page), we are on page %s", total_items, items_on_page, current_page) # But don't show them if not episodes were found if self.episodes: if items_on_page < 50: Logger.debug("No more pages to show.") else: next_page = current_page + 1 url = self.parentItem.url[:self.parentItem.url.rindex("=")] url = "%s=%s" % (url, next_page) Logger.trace(url) page_item = MediaItem(str(next_page), url) page_item.type = "page" page_item.complete = True items.append(page_item) return data, items
def run_addon(): """ Runs Retrospect as a Video Add-On """ log_file = None try: from resources.lib.retroconfig import Config from resources.lib.helpers.sessionhelper import SessionHelper # get a logger up and running from resources.lib.logger import Logger # only append if there are no active sessions if not SessionHelper.is_session_active(): # first call in the session, so do not append the log append_log_file = False else: append_log_file = True log_file = Logger.create_logger(os.path.join(Config.profileDir, Config.logFileNameAddon), Config.appName, append=append_log_file, dual_logger=lambda x, y=4: xbmc.log(x, y)) from resources.lib.urihandler import UriHandler from resources.lib.addonsettings import AddonSettings AddonSettings.set_language() from resources.lib.textures import TextureHandler # update the loglevel Logger.instance().minLogLevel = AddonSettings.get_log_level() use_caching = AddonSettings.cache_http_responses() cache_dir = None if use_caching: cache_dir = Config.cacheDir ignore_ssl_errors = AddonSettings.ignore_ssl_errors() UriHandler.create_uri_handler(cache_dir=cache_dir, cookie_jar=os.path.join(Config.profileDir, "cookiejar.dat"), ignore_ssl_errors=ignore_ssl_errors) # start texture handler TextureHandler.set_texture_handler(Config, Logger.instance(), UriHandler.instance()) # run the plugin from resources.lib import plugin plugin.Plugin(sys.argv[0], sys.argv[2], sys.argv[1]) # make sure we leave no references behind AddonSettings.clear_cached_addon_settings_object() # close the log to prevent locking on next call Logger.instance().close_log() log_file = None except: if log_file: log_file.critical("Error running plugin", exc_info=True) log_file.close_log() raise
def update_video_item(self, item): """ Updates an existing MediaItem with more data. Used to update none complete MediaItems (self.complete = False). This could include opening the item's URL to fetch more data and then process that data or retrieve it's real media-URL. The method should at least: * cache the thumbnail to disk (use self.noImage if no thumb is available). * set at least one MediaItemPart with a single MediaStream. * set self.complete = True. if the returned item does not have a MediaItemPart then the self.complete flag will automatically be set back to False. :param MediaItem item: the original MediaItem that needs updating. :return: The original item with more data added to it's properties. :rtype: MediaItem """ Logger.debug('Starting update_video_item for %s (%s)', item.name, self.channelName) # noinspection PyStatementEffect """ <script type="text/javascript">/* <![CDATA[ */ var movieFlashVars = " image=http://assets.ur.se/id/147834/images/1_l.jpg file=/147000-147999/147834-20.mp4 plugins=http://urplay.se/jwplayer/plugins/gapro-1.swf,http://urplay.se/jwplayer/plugins/sharing-2.swf,http://urplay.se/jwplayer/plugins/captions/captions.swf sharing.link=http://urplay.se/147834 gapro.accountid=UA-12814852-8 captions.margin=40 captions.fontsize=11 captions.back=false captions.file=http://undertexter.ur.se/147000-147999/147834-19.tt streamer=rtmp://streaming.ur.se/ondemand autostart=False"; var htmlVideoElementSource = "http://streaming.ur.se/ondemand/mp4:147834-23.mp4/playlist.m3u8?location=SE"; /* //]]> */ </script> """ data = UriHandler.open(item.url) # Extract stream JSON data from HTML streams = Regexer.do_regex( r'ProgramContainer" data-react-props="({[^"]+})"', data) json_data = streams[0] json_data = HtmlEntityHelper.convert_html_entities(json_data) json = JsonHelper(json_data, logger=Logger.instance()) Logger.trace(json.json) item.MediaItemParts = [] # generic server information proxy_data = UriHandler.open( "https://streaming-loadbalancer.ur.se/loadbalancer.json", no_cache=True) proxy_json = JsonHelper(proxy_data) proxy = proxy_json.get_value("redirect") Logger.trace("Found RTMP Proxy: %s", proxy) stream_infos = json.get_value("program", "streamingInfo") part = item.create_new_empty_media_part() for stream_type, stream_info in stream_infos.items(): Logger.trace(stream_info) default_stream = stream_info.get("default", False) bitrates = { "mp3": 400, "m4a": 250, "sd": 1200, "hd": 2000, "tt": None } for quality, bitrate in bitrates.items(): stream = stream_info.get(quality) if stream is None: continue stream_url = stream["location"] if quality == "tt": part.Subtitle = SubtitleHelper.download_subtitle( stream_url, format="ttml") continue bitrate = bitrate if default_stream else bitrate + 1 if stream_type == "raw": bitrate += 1 url = "https://%s/%smaster.m3u8" % (proxy, stream_url) part.append_media_stream(url, bitrate) item.complete = True return item
def process_live_items(self, data): # NOSONAR """ Performs pre-process actions that either return multiple live channels that are present in the live url or an actual list item if a single live stream is present. Accepts an data from the process_folder_list method, BEFORE the items are processed. Allows setting of parameters (like title etc) for the channel. Inside this method the <data> could be changed and additional items can be created. The return values should always be instantiated in at least ("", []). :param str data: The retrieve data that was loaded for the current item and URL. :return: A tuple of the data and a list of MediaItems that were generated. :rtype: tuple[str|JsonHelper,list[MediaItem]] """ items = [] Logger.info("Adding Live Streams") if self.liveUrl.endswith(".m3u8"): # We actually have a single stream. title = "{} - {}".format( self.channelName, LanguageHelper.get_localized_string( LanguageHelper.LiveStreamTitleId)) live_item = MediaItem(title, self.liveUrl) live_item.type = 'video' live_item.isLive = True if self.channelCode == "rtvdrenthe": # RTV Drenthe actually has a buggy M3u8 without master index. live_item.append_single_stream(live_item.url, 0) live_item.complete = True items.append(live_item) return "", items # we basically will check for live channels json_data = JsonHelper(data, logger=Logger.instance()) live_streams = json_data.get_value() Logger.trace(live_streams) if "videos" in live_streams: Logger.debug("Multiple streams found") live_streams = live_streams["videos"] elif not isinstance(live_streams, (list, tuple)): Logger.debug("Single streams found") live_streams = (live_streams, ) else: Logger.debug("List of stream found") live_stream_value = None for streams in live_streams: Logger.debug("Adding live stream") title = streams.get( 'name') or "%s - Live TV" % (self.channelName, ) live_item = MediaItem(title, self.liveUrl) live_item.type = 'video' live_item.complete = True live_item.isLive = True part = live_item.create_new_empty_media_part() for stream in streams: Logger.trace(stream) bitrate = None # used in Omrop Fryslan if stream == "android" or stream == "iPhone": bitrate = 250 url = streams[stream]["videoLink"] elif stream == "iPad": bitrate = 1000 url = streams[stream]["videoLink"] # used in RTV Utrecht elif stream == "androidLink" or stream == "iphoneLink": bitrate = 250 url = streams[stream] elif stream == "ipadLink": bitrate = 1000 url = streams[stream] elif stream == "tabletLink": bitrate = 300 url = streams[stream] # These windows stream won't work # elif stream == "windowsLink": # bitrate = 1200 # url = streams[stream] # elif stream == "wpLink": # bitrate = 1200 # url = streams[stream] elif stream == "name": Logger.trace("Ignoring stream '%s'", stream) else: Logger.warning("No url found for type '%s'", stream) # noinspection PyUnboundLocalVariable if "livestreams.omroep.nl/live/" in url and url.endswith( "m3u8"): Logger.info("Found NPO Stream, adding ?protection=url") url = "%s?protection=url" % (url, ) if bitrate: part.append_media_stream(url, bitrate) if url == live_stream_value and ".m3u8" in url: # if it was equal to the previous one, assume we have a m3u8. Reset the others. Logger.info( "Found same M3u8 stream for all streams for this Live channel, using that one: %s", url) live_item.MediaItemParts = [] live_item.url = url live_item.complete = False break elif "playlist.m3u8" in url: # if we have a playlist, use that one. Reset the others. Logger.info( "Found M3u8 playlist for this Live channel, using that one: %s", url) live_item.MediaItemParts = [] live_item.url = url live_item.complete = False break else: # add it to the possibilities live_stream_value = url items.append(live_item) return "", items
def __init__(self, addon_name, params, handle=0): """ Initialises the plugin with given arguments. :param str addon_name: The add-on name. :param str params: The input parameters from the query string. :param int|str handle: The Kodi directory handle. """ Logger.info("******** Starting %s add-on version %s/repo *********", Config.appName, Config.version) # noinspection PyTypeChecker super(Plugin, self).__init__(addon_name, handle, params) Logger.debug(self) # are we in session? session_active = SessionHelper.is_session_active(Logger.instance()) # fetch some environment settings env_ctrl = envcontroller.EnvController(Logger.instance()) if not session_active: # do add-on start stuff Logger.info("Add-On start detected. Performing startup actions.") # print the folder structure env_ctrl.print_retrospect_settings_and_folders( Config, AddonSettings) # show notification XbmcWrapper.show_notification(None, LanguageHelper.get_localized_string( LanguageHelper.StartingAddonId) % (Config.appName, ), fallback=False, logger=Logger) # check for updates. Using local import for performance from resources.lib.updater import Updater up = Updater(Config.updateUrl, Config.version, UriHandler.instance(), Logger.instance(), AddonSettings.get_release_track()) if up.is_new_version_available(): Logger.info("Found new version online: %s vs %s", up.currentVersion, up.onlineVersion) notification = LanguageHelper.get_localized_string( LanguageHelper.NewVersion2Id) notification = notification % (Config.appName, up.onlineVersion) XbmcWrapper.show_notification(None, lines=notification, display_time=20000) # check for cache folder env_ctrl.cache_check() # do some cache cleanup env_ctrl.cache_clean_up(Config.cacheDir, Config.cacheValidTime) # empty picklestore self.pickler.purge_store(Config.addonId) # create a session SessionHelper.create_session(Logger.instance())
def get_streams_from_npo(url, episode_id, proxy=None, headers=None): """ Retrieve NPO Player Live streams from a different number of stream urls. @param url: (String) The url to download @param episode_id: (String) The NPO episode ID @param headers: (dict) Possible HTTP Headers @param proxy: (Proxy) The proxy to use for opening Can be used like this: part = item.create_new_empty_media_part() for s, b in NpoStream.get_streams_from_npo(m3u8Url, self.proxy): item.complete = True # s = self.get_verifiable_video_url(s) part.append_media_stream(s, b) """ if url: Logger.info("Determining streams for url: %s", url) episode_id = url.split("/")[-1] elif episode_id: Logger.info("Determining streams for VideoId: %s", episode_id) else: Logger.error("No url or streamId specified!") return [] # we need an hash code token_json_data = UriHandler.open("http://ida.omroep.nl/app.php/auth", no_cache=True, proxy=proxy, additional_headers=headers) token_json = JsonHelper(token_json_data) token = token_json.get_value("token") url = "http://ida.omroep.nl/app.php/%s?adaptive=yes&token=%s" % ( episode_id, token) stream_data = UriHandler.open(url, proxy=proxy, additional_headers=headers) if not stream_data: return [] stream_json = JsonHelper(stream_data, logger=Logger.instance()) stream_infos = stream_json.get_value("items")[0] Logger.trace(stream_infos) streams = [] for stream_info in stream_infos: Logger.debug("Found stream info: %s", stream_info) if stream_info["format"] == "mp3": streams.append((stream_info["url"], 0)) continue elif stream_info["contentType"] == "live": Logger.debug("Found live stream") url = stream_info["url"] url = url.replace("jsonp", "json") live_url_data = UriHandler.open(url, proxy=proxy, additional_headers=headers) live_url = live_url_data.strip("\"").replace("\\", "") Logger.trace(live_url) streams += M3u8.get_streams_from_m3u8(live_url, proxy, headers=headers) elif stream_info["format"] == "hls": m3u8_info_url = stream_info["url"] m3u8_info_data = UriHandler.open(m3u8_info_url, proxy=proxy, additional_headers=headers) m3u8_info_json = JsonHelper(m3u8_info_data, logger=Logger.instance()) m3u8_url = m3u8_info_json.get_value("url") streams += M3u8.get_streams_from_m3u8(m3u8_url, proxy, headers=headers) elif stream_info["format"] == "mp4": bitrates = {"hoog": 1000, "normaal": 500} url = stream_info["url"] if "contentType" in stream_info and stream_info[ "contentType"] == "url": mp4_url = url else: url = url.replace("jsonp", "json") mp4_url_data = UriHandler.open(url, proxy=proxy, additional_headers=headers) mp4_info_json = JsonHelper(mp4_url_data, logger=Logger.instance()) mp4_url = mp4_info_json.get_value("url") bitrate = bitrates.get(stream_info["label"].lower(), 0) if bitrate == 0 and "/ipod/" in mp4_url: bitrate = 200 elif bitrate == 0 and "/mp4/" in mp4_url: bitrate = 500 streams.append((mp4_url, bitrate)) return streams