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
Ejemplo n.º 2
0
    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(": "))
Ejemplo n.º 3
0
    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
Ejemplo n.º 4
0
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(": "))
Ejemplo n.º 5
0
    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)
Ejemplo n.º 7
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)
Ejemplo n.º 8
0
    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)
Ejemplo n.º 9
0
    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
Ejemplo n.º 10
0
    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
Ejemplo n.º 11
0
    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
Ejemplo n.º 12
0
    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()
Ejemplo n.º 13
0
    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)
Ejemplo n.º 14
0
    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)
Ejemplo n.º 15
0
    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
Ejemplo n.º 16
0
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
Ejemplo n.º 17
0
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
Ejemplo n.º 18
0
    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)
Ejemplo n.º 19
0
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
Ejemplo n.º 21
0
    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
Ejemplo n.º 22
0
    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
Ejemplo n.º 23
0
    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
Ejemplo n.º 24
0
    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
Ejemplo n.º 25
0
    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
Ejemplo n.º 26
0
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
Ejemplo n.º 27
0
    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
Ejemplo n.º 28
0
    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
Ejemplo n.º 29
0
    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())
Ejemplo n.º 30
0
    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