class MediaItem: """Main class that represent items that are retrieved in XOT. They are used to fill the lists and have MediaItemParts which have MediaStreams in this hierarchy: MediaItem +- MediaItemPart | +- MediaStream | +- MediaStream | +- MediaStream +- MediaItemPart | +- MediaStream | +- MediaStream | +- MediaStream """ LabelTrackNumber = "TrackNumber" LabelDuration = "Duration" ExpiresAt = LanguageHelper.get_localized_string(LanguageHelper.ExpiresAt) def __dir__(self): """ Required in order for the Pickler().Validate to work! """ return [ "name", "url", "actionUrl", "MediaItemParts", "description", "thumb", "fanart", "icon", "__date", "__timestamp", "type", "dontGroup", "isLive", "isGeoLocked", "isDrmProtected", "isPaid", "__infoLabels", "complete", "items", "HttpHeaders", "guid", "guidValue" ] #noinspection PyShadowingBuiltins def __init__(self, title, url, type="folder"): """ Creates a new MediaItem. The `url` can contain an url to a site more info about the item can be retrieved, for instance for a video item to retrieve the media url, or in case of a folder where child items can be retrieved. Essential is that no encoding (like UTF8) is specified in the title of the item. This is all taken care of when creating Kodi items in the different methods. :param str|unicode title: The title of the item, used for appearance in lists. :param str|unicode url: Url that used for further information retrieval. :param str type: Type of MediaItem (folder, video, audio). Defaults to 'folder'. """ name = title.strip() self.name = name self.url = url self.actionUrl = None self.MediaItemParts = [] self.description = "" self.thumb = "" # : The local or remote image for the thumbnail of episode self.fanart = "" # : The fanart url self.icon = "" # : low quality icon for list self.__date = "" # : value show in interface self.__timestamp = datetime.min # : value for sorting, this one is set to minimum so if non is set, it's shown at the bottom self.__expires_datetime = None # : datetime value of the expire time self.type = type # : video, audio, folder, append, page, playlist self.dontGroup = False # : if set to True this item will not be auto grouped. self.isLive = False # : if set to True, the item will have a random QuerySting param self.isGeoLocked = False # : if set to True, the item is GeoLocked to the channels language (o) self.isDrmProtected = False # : if set to True, the item is DRM protected and cannot be played (^) self.isPaid = False # : if set to True, the item is a Paid item and cannot be played (*) self.__infoLabels = dict() # : Additional Kodi InfoLabels self.complete = False self.items = [] self.HttpHeaders = dict() # : http headers for the item data retrieval # Items that are not essential for pickled self.isCloaked = False self.metaData = dict( ) # : Additional data that is for internal / routing use only # GUID used for identifcation of the object. Do not set from script, MD5 needed # to prevent UTF8 issues try: self.guid = "%s%s" % (EncodingHelper.encode_md5(title), EncodingHelper.encode_md5(url or "")) except: Logger.error( "Error setting GUID for title:'%s' and url:'%s'. Falling back to UUID", title, url, exc_info=True) self.guid = self.__get_uuid() self.guidValue = int("0x%s" % (self.guid, ), 0) def append_single_stream(self, url, bitrate=0, subtitle=None): """ Appends a single stream to a new MediaPart of this MediaItem. This methods creates a new MediaPart item and adds the provided stream to its MediaStreams collection. The newly created MediaPart is then added to the MediaItem's MediaParts collection. :param str url: Url of the stream. :param int bitrate: Bitrate of the stream (default = 0). :param str subtitle: Url of the subtitle of the mediapart. :return: A reference to the created MediaPart. :rtype: MediaItemPart """ new_part = MediaItemPart(self.name, url, bitrate, subtitle) self.MediaItemParts.append(new_part) return new_part def create_new_empty_media_part(self): """ Adds an empty MediaPart to the MediaItem. This method is used to create an empty MediaPart that can be used to add new stream to. The newly created MediaPart is appended to the MediaItem.MediaParts list. :return: The new MediaPart object (as a reference) that was appended. :rtype: MediaItemPart """ new_part = MediaItemPart(self.name) self.MediaItemParts.append(new_part) return new_part def has_media_item_parts(self): """ Return True if there are any MediaItemParts present with streams for this MediaItem :return: True if there are any MediaItemParts present with streams for this MediaItem :rtype: bool """ for part in self.MediaItemParts: if len(part.MediaStreams) > 0: return True return False def is_playable(self): """ Returns True if the item can be played in a Media Player. At this moment it returns True for: * type = 'video' * type = 'audio' :return: Returns true if this is a playable MediaItem :rtype: bool """ return self.type.lower() in ('video', 'audio', 'playlist') def has_track(self): """ Does this MediaItem have a TrackNumber InfoLabel :return: if the track was set. :rtype: bool """ return MediaItem.LabelTrackNumber in self.__infoLabels def has_date(self): """ Returns if a date was set :return: True if a date was set. :rtype: bool """ return self.__timestamp > datetime.min def clear_date(self): """ Resets the date (used for favourites for example). """ self.__timestamp = datetime.min self.__date = "" def has_info(self): """ Indicator to show that this item has additional InfoLabels :return: whether or not there are infolabels :rtype: bool """ return bool(self.__infoLabels) def set_info_label(self, label, value): """ Set a Kodi InfoLabel and its value. See http://kodi.wiki/view/InfoLabels :param str label: the name of the label :param Any value: the value to assign """ self.__infoLabels[label] = value def set_season_info(self, season, episode): """ Set season and episode information :param str|int season: The Season Number :param str|int episode: The Episode Number """ if season is None or episode is None: Logger.warning("Cannot set EpisodeInfo without season and episode") return self.__infoLabels["Episode"] = int(episode) self.__infoLabels["Season"] = int(season) return def set_expire_datetime(self, timestamp, year=0, month=0, day=0, hour=0, minutes=0, seconds=0): """ Sets the datetime value until when the item can be streamed. :param datetime|None timestamp: A full datetime object. :param int|str year: The year of the datetime. :param int|str month: The month of the datetime. :param int|str day: The day of the datetime. :param int|str|None hour: The hour of the datetime (Optional) :param int|str|None minutes: The minutes of the datetime (Optional) :param int|str|None seconds: The seconds of the datetime (Optional) """ if timestamp is not None: self.__expires_datetime = timestamp return self.__expires_datetime = datetime(int(year), int(month), int(day), int(hour), int(minutes), int(seconds)) def set_date(self, year, month, day, hour=None, minutes=None, seconds=None, only_if_newer=False, text=None): """ Sets the datetime of the MediaItem. Sets the datetime of the MediaItem in the self.__date and the corresponding text representation of that datetime. `hour`, `minutes` and `seconds` can be optional and will be set to 0 in that case. They must all be set or none of them. Not just one or two of them. If `only_if_newer` is set to True, the update will only occur if the set datetime is newer then the currently set datetime. The text representation can be overwritten by setting the `text` keyword to a specific value. In that case the timestamp is set to the given time values but the text representation will be overwritten. If the values form an invalid datetime value, the datetime value will be reset to their default values. :param int|str year: The year of the datetime. :param int|str month: The month of the datetime. :param int|str day: The day of the datetime. :param int|str|none hour: The hour of the datetime (Optional) :param int|str|none minutes: The minutes of the datetime (Optional) :param int|str|none seconds: The seconds of the datetime (Optional) :param bool only_if_newer: Update only if the new date is more recent then the currently set one :param str text: If set it will overwrite the text in the date label the datetime is also set. :return: The datetime that was set. :rtype: datetime """ # date_format = xbmc.getRegion('dateshort') # correct a small bug in Kodi # date_format = date_format[1:].replace("D-M-", "%D-%M") # dateFormatLong = xbmc.getRegion('datelong') # timeFormat = xbmc.getRegion('time') # date_time_format = "%s %s" % (date_format, timeFormat) try: date_format = "%Y-%m-%d" # "%x" date_time_format = date_format + " %H:%M" if hour is None and minutes is None and seconds is None: time_stamp = datetime(int(year), int(month), int(day)) date = time_stamp.strftime(date_format) else: time_stamp = datetime(int(year), int(month), int(day), int(hour), int(minutes), int(seconds)) date = time_stamp.strftime(date_time_format) if only_if_newer and self.__timestamp > time_stamp: return self.__timestamp = time_stamp if text is None: self.__date = date else: self.__date = text except ValueError: Logger.error( "Error setting date: Year=%s, Month=%s, Day=%s, Hour=%s, Minutes=%s, Seconds=%s", year, month, day, hour, minutes, seconds, exc_info=True) self.__timestamp = datetime.min self.__date = "" return self.__timestamp def get_kodi_item(self, name=None): """Creates a Kodi item with the same data is the MediaItem. This item is used for displaying purposes only and changes to it will not be passed on to the MediaItem. :param str|unicode name: Overwrites the name of the Kodi item. :return: a complete Kodi ListItem :rtype: xbmcgui.ListItem """ # Update name and descriptions name_post_fix, description_pre_fix = self.__update_title_and_description_with_limitations( ) name = self.__get_title(name) name = "%s %s" % (name, name_post_fix) name = self.__full_decode_text(name) if self.description is None: self.description = '' if description_pre_fix != "": description = "%s\n\n%s" % (description_pre_fix, self.description) else: description = self.description description = self.__full_decode_text(description) if description is None: description = "" # the Kodi ListItem date # date: string (%d.%m.%Y / 01.01.2009) - file date if self.__timestamp > datetime.min: kodi_date = self.__timestamp.strftime("%d.%m.%Y") kodi_year = self.__timestamp.year else: kodi_date = "" kodi_year = 0 # Get all the info labels starting with the ones set and then add the specific ones info_labels = self.__infoLabels.copy() info_labels["Title"] = name if kodi_date: info_labels["Date"] = kodi_date info_labels["Year"] = kodi_year info_labels["Aired"] = kodi_date if self.type != "audio": info_labels["Plot"] = description # now create the Kodi item item = xbmcgui.ListItem(name or "<unknown>", self.__date) item.setLabel(name) item.setLabel2(self.__date) # set a flag to indicate it is a item that can be used with setResolveUrl. if self.is_playable(): Logger.trace("Setting IsPlayable to True") item.setProperty("IsPlayable", "true") # specific items Logger.trace("Setting InfoLabels: %s", info_labels) if self.type == "audio": item.setInfo(type="music", infoLabels=info_labels) else: item.setInfo(type="video", infoLabels=info_labels) # now set all the art to prevent duplicate calls to Kodi if self.fanart and not AddonSettings.hide_fanart(): item.setArt({ 'thumb': self.thumb, 'icon': self.icon, 'fanart': self.fanart }) else: item.setArt({'thumb': self.thumb, 'icon': self.icon}) # Set Artwork # art = dict() # for l in ("thumb", "poster", "banner", "fanart", "clearart", "clearlogo", "landscape"): # art[l] = self.thumb # item.setArt(art) # We never set the content resolving, Retrospect does this. And if we do, then the custom # headers are removed from the URL when opening the resolved URL. try: item.setContentLookup(False) except: # apparently not yet supported on this Kodi version3 pass return item def get_kodi_play_list_data(self, bitrate, proxy=None): """ Returns the playlist items for this MediaItem :param int bitrate: The bitrate of the streams that should be in the playlist. Given in kbps. :param ProxyInfo|None proxy: The proxy to set :return: A list of ListItems that should be added to a playlist with their selected stream url :rtype: list[tuple[xbmcgui.ListItem, str]] """ Logger.info("Creating playlist items for Bitrate: %s kbps\n%s", bitrate, self) if not bool(bitrate): raise ValueError("Bitrate not specified") play_list_data = [] for part in self.MediaItemParts: if len(part.MediaStreams) == 0: Logger.warning("Ignoring empty MediaPart: %s", part) continue kodi_item = self.get_kodi_item() stream = part.get_media_stream_for_bitrate(bitrate) Logger.info("Selected Stream: %s", stream) if stream.Adaptive: Adaptive.set_max_bitrate(stream, max_bit_rate=bitrate) # Set the actual stream path kodi_item.setProperty("path", stream.Url) # properties of the Part for prop in part.Properties + stream.Properties: Logger.trace("Adding property: %s", prop) kodi_item.setProperty(prop[0], prop[1]) # TODO: Apparently if we use the InputStream Adaptive, using the setSubtitles() causes sync issues. if part.Subtitle and False: Logger.debug("Adding subtitle to ListItem: %s", part.Subtitle) kodi_item.setSubtitles([ part.Subtitle, ]) # Set any custom Header header_params = dict() # set proxy information if present self.__set_kodi_proxy_info(kodi_item, stream, stream.Url, header_params, proxy) # Now add the actual HTTP headers for k in part.HttpHeaders: header_params[k] = HtmlEntityHelper.url_encode( part.HttpHeaders[k]) stream_url = stream.Url if header_params: kodi_query_string = reduce( lambda x, y: "%s&%s=%s" % (x, y, header_params[y]), header_params.keys(), "") kodi_query_string = kodi_query_string.lstrip("&") Logger.debug("Adding Kodi Stream parameters: %s\n%s", header_params, kodi_query_string) stream_url = "%s|%s" % (stream.Url, kodi_query_string) play_list_data.append((kodi_item, stream_url)) return play_list_data @property def uses_external_addon(self): return self.url is not None and self.url.startswith("plugin://") def __set_kodi_proxy_info(self, kodi_item, stream, stream_url, kodi_params, proxy): """ Updates a Kodi ListItem with the correct Proxy configuration taken from the ProxyInfo object. :param xbmcgui.ListItem kodi_item: The current Kodi ListItem. :param MediaStream stream: The current Stream object. :param str stream_url: The current Url for the Stream object (might have been changed in the mean time by other calls) :param dict[str|unicode,str] kodi_params: A dictionary of Kodi Parameters. :param ProxyInfo proxy: The ProxyInfo object """ if not proxy: return if proxy.Scheme.startswith( "http") and not stream.Url.startswith("http"): Logger.debug("Not adding proxy due to scheme mismatch") elif proxy.Scheme == "dns": Logger.debug("Not adding DNS proxy for Kodi streams") elif not proxy.use_proxy_for_url(stream_url): Logger.debug("Not adding proxy due to filter mismatch") else: if AddonSettings.is_min_version(17): # See ffmpeg proxy in https://github.com/xbmc/xbmc/commit/60b21973060488febfdc562a415e11cb23eb9764 kodi_item.setProperty("proxy.host", proxy.Proxy) kodi_item.setProperty("proxy.port", str(proxy.Port)) kodi_item.setProperty("proxy.type", proxy.Scheme) if proxy.Username: kodi_item.setProperty("proxy.user", proxy.Username) if proxy.Password: kodi_item.setProperty("proxy.password", proxy.Password) Logger.debug("Adding (Krypton) %s", proxy) else: kodi_params["HttpProxy"] = proxy.get_proxy_address() Logger.debug("Adding (Pre-Krypton) %s", proxy) return def __get_uuid(self): """ Generates a Unique Identifier based on Time and Random Integers """ return binascii.hexlify(os.urandom(16)).upper() def __full_decode_text(self, string_value): """ Decodes a byte encoded string with HTML content into Unicode String Arguments: stringValue : string - The byte encoded string to decode Returns: An Unicode String with all HTML entities replaced by their UTF8 characters The decoding is done by first decode the string to UTF8 and then replace the HTML entities to their UTF8 characters. """ if string_value is None: return None if string_value == "": return "" # then get rid of the HTML entities string_value = HtmlEntityHelper.convert_html_entities(string_value) return string_value def __str__(self): """ String representation :return: The String representation :rtype: str """ value = self.name if self.is_playable(): if len(self.MediaItemParts) > 0: value = "MediaItem: %s [Type=%s, Complete=%s, IsLive=%s, Date=%s, Geo/DRM=%s/%s]" % \ (value, self.type, self.complete, self.isLive, self.__date, self.isGeoLocked, self.isDrmProtected) for media_part in self.MediaItemParts: value = "%s\n%s" % (value, media_part) value = "%s" % (value, ) else: value = "%s [Type=%s, Complete=%s, unknown urls, IsLive=%s, Date=%s, Geo/DRM=%s/%s]" \ % (value, self.type, self.complete, self.isLive, self.__date, self.isGeoLocked, self.isDrmProtected) else: value = "%s [Type=%s, Url=%s, Date=%s, IsLive=%s, Geo/DRM=%s/%s]" \ % (value, self.type, self.url, self.__date, self.isLive, self.isGeoLocked, self.isDrmProtected) return value def __eq__(self, item): """ checks 2 items for Equality Arguments: item : MediaItem - The item to check for equality. Returns: the output of self.__equals(item). """ return self.__equals(item) def __ne__(self, item): """ returns NOT Equal Arguments: item : MediaItem - The item to check for equality. Returns: the output of not self.__equals(item). """ return not self.__equals(item) def __hash__(self): """ returns the hash value """ return hash(self.guidValue) def __equals(self, other): """ Checks two MediaItems for equality :param MediaItem other: The other item. :return: whether the objects are equal (if the item's GUID's match). :rtype: bool """ if not other: return False return self.guidValue == other.guidValue def __update_title_and_description_with_limitations(self): """ Updates the title/name and description with the symbols for DRM, GEO and Paid. :return: (tuple) name postfix, description postfix :rtype: tuple[str,str] """ geo_lock = "º" # º drm_lock = "^" # ^ paid = "ª" # ª cloaked = "¨" # ¨ description_prefix = [] title_postfix = [] description = "" title = "" if self.__expires_datetime is not None: expires = "{}: {}".format( MediaItem.ExpiresAt, self.__expires_datetime.strftime("%Y-%m-%d %H:%M")) description_prefix.append(expires) if self.isDrmProtected: title_postfix.append(drm_lock) description_prefix.append( LanguageHelper.get_localized_string( LanguageHelper.DrmProtected)) if self.isGeoLocked: title_postfix.append(geo_lock) description_prefix.append( LanguageHelper.get_localized_string( LanguageHelper.GeoLockedId)) if self.isPaid: title_postfix.append(paid) description_prefix.append( LanguageHelper.get_localized_string( LanguageHelper.PremiumPaid)) if self.isCloaked: title_postfix.append(cloaked) description_prefix.append( LanguageHelper.get_localized_string(LanguageHelper.HiddenItem)) if self.uses_external_addon: from resources.lib.xbmcwrapper import XbmcWrapper external = XbmcWrapper.get_external_add_on_label(self.url) title_postfix.append(external) # actually update it if description_prefix: description_prefix = "\n".join(description_prefix) description = "[COLOR gold][I]%s[/I][/COLOR]" % ( description_prefix.rstrip(), ) if title_postfix: title = "".join(title_postfix) title = "[COLOR gold]%s[/COLOR]" % (title.lstrip(), ) return title, description def __get_title(self, name): """ Create the title based on the MediaItems name and type. :param str name: the name to update. :return: an updated name :rtype: str """ if not name: name = self.name if self.type == 'page': # We need to add the Page prefix to the item name = "%s %s" % (LanguageHelper.get_localized_string( LanguageHelper.Page), name) Logger.debug("MediaItem.__get_title :: Adding Page Prefix") elif self.__date != '' and not self.is_playable( ) and not AddonSettings.is_min_version(18): # not playable items should always show date name = "%s [COLOR=dimgray](%s)[/COLOR]" % (name, self.__date) folder_prefix = AddonSettings.get_folder_prefix() if self.type == "folder" and not folder_prefix == "": name = "%s %s" % (folder_prefix, name) return name def __setstate__(self, state): """ Sets the current MediaItem's state based on the pickled value. However, it also adds newly added class variables so old items won't brake. @param state: a default Pickle __dict__ """ # creating a new MediaItem here should not cause too much performance issues, as not very many # will be depickled. m = MediaItem("", "") self.__dict__ = m.__dict__ self.__dict__.update(state)
def get_channels(self, include_disabled=False, **kwargs): # NOSONAR """ Retrieves all enabled channels within Retrospect. If updated channels are found, the those channels are indexed and the channel index is rebuild. :param bool include_disabled: Boolean to indicate if we should include those channels that are explicitly disabled from the settings. :param dict kwargs: Here for backward compatibility. :return: a list of ChannelInfo objects of enabled channels. :rtype: list[ChannelInfo] """ sw = StopWatch("ChannelIndex.get_channels Importer", Logger.instance()) Logger.info("Fetching all enabled channels.") self.__allChannels = [] valid_channels = [] # What platform are we platform = envcontroller.EnvController.get_platform() channels_updated = False country_visibility = {} for channel_set in self.__channelIndex[ self.__CHANNEL_INDEX_CHANNEL_KEY]: channel_set = self.__channelIndex[ self.__CHANNEL_INDEX_CHANNEL_KEY][channel_set] channel_set_info_path = channel_set[ self.__CHANNEL_INDEX_CHANNEL_INFO_KEY] channel_set_version = channel_set[ self.__CHANNEL_INDEX_CHANNEL_VERSION_KEY] # Check if file exists. If not, rebuild index if not os.path.isfile( channel_set_info_path) and not self.__reindexed: Logger.warning("Missing channelSet file: %s.", channel_set_info_path) self.__rebuild_index() return self.get_channels() channel_infos = ChannelInfo.from_json(channel_set_info_path, channel_set_version) # Check if the channel was updated if self.__is_channel_set_updated(channel_infos[0]): # let's see if the index has already been updated this section, of not, do it and # restart the ChannelRetrieval. if not self.__reindexed: # rebuild and restart Logger.warning( "Re-index channel index due to channelSet update: %s.", channel_set_info_path) self.__rebuild_index() return self.get_channels() else: Logger.warning("Found updated channelSet: %s.", channel_set_info_path) if not channels_updated: # this was the first update found (otherwise channelsUpdated was True) show a message: title = LanguageHelper.get_localized_string( LanguageHelper.InitChannelTitle) text = LanguageHelper.get_localized_string( LanguageHelper.InitChannelText) XbmcWrapper.show_notification(title, text, display_time=15000, logger=Logger.instance()) channels_updated |= True # Initialise the channelset. self.__initialise_channel_set(channel_infos[0]) # And perform all first actions for the included channels in the set for channel_info in channel_infos: self.__initialise_channel(channel_info) # Check the channel validity for channel_info in channel_infos: if not self.__channel_is_correct(channel_info): continue self.__allChannels.append(channel_info) # valid channel for this platform ? if not channel_info.compatiblePlatforms & platform == platform: Logger.warning( "Not loading: %s -> platform '%s' is not compatible.", channel_info, Environments.name(platform)) continue valid_channels.append(channel_info) # was the channel hidden based on language settings? We do some caching to speed # things up. if channel_info.language not in country_visibility: country_visibility[ channel_info. language] = AddonSettings.show_channel_with_language( channel_info.language) channel_info.visible = country_visibility[ channel_info.language] # was the channel explicitly disabled from the settings? channel_info.enabled = AddonSettings.get_channel_visibility( channel_info) Logger.debug("Found channel: %s", channel_info) if channels_updated: Logger.info( "New or updated channels found. Updating add-on configuration for all channels and user agent." ) AddonSettings.update_add_on_settings_with_channels( valid_channels, Config) AddonSettings.update_user_agent() else: Logger.debug( "No channel changes found. Skipping add-on configuration for channels." ) # TODO: perhaps we should check that the settings.xml is correct and not broken? valid_channels.sort(key=lambda c: c.sort_key) visible_channels = [ ci for ci in valid_channels if ci.visible and ci.enabled ] Logger.info("Fetch a total of %d channels of which %d are visible.", len(valid_channels), len(visible_channels)) sw.stop() if include_disabled: return valid_channels return visible_channels
def add_live_items_and_genres(self, data): """ Adds the Live items, Channels and Last Episodes to the listing. :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 = [] extra_items = { LanguageHelper.get_localized_string(LanguageHelper.LiveTv): "https://www.svtplay.se/kanaler", LanguageHelper.get_localized_string(LanguageHelper.CurrentlyPlayingEpisodes): self.__get_api_url( "GridPage", "265677a2465d93d39b536545cdc3664d97e3843ce5e34f145b2a45813b85007b", variables={"selectionId": "live"}), LanguageHelper.get_localized_string(LanguageHelper.Search): "searchSite", LanguageHelper.get_localized_string(LanguageHelper.Recent): self.__get_api_url( "GridPage", "265677a2465d93d39b536545cdc3664d97e3843ce5e34f145b2a45813b85007b", variables={"selectionId": "latest"}), LanguageHelper.get_localized_string(LanguageHelper.LastChance): self.__get_api_url( "GridPage", "265677a2465d93d39b536545cdc3664d97e3843ce5e34f145b2a45813b85007b", variables={"selectionId": "lastchance"}), LanguageHelper.get_localized_string(LanguageHelper.MostViewedEpisodes): self.__get_api_url( "GridPage", "265677a2465d93d39b536545cdc3664d97e3843ce5e34f145b2a45813b85007b", variables={"selectionId": "popular"}), } for title, url in extra_items.items(): new_item = MediaItem("\a.: %s :." % (title, ), url) new_item.complete = True new_item.thumb = self.noImage new_item.dontGroup = True items.append(new_item) genre_tags = "\a.: {}/{} :.".format( LanguageHelper.get_localized_string(LanguageHelper.Genres), LanguageHelper.get_localized_string(LanguageHelper.Tags).lower()) genre_url = self.__get_api_url( "AllGenres", "6bef51146d05b427fba78f326453127f7601188e46038c9a5c7b9c2649d4719c", {}) genre_item = MediaItem(genre_tags, genre_url) genre_item.complete = True genre_item.thumb = self.noImage genre_item.dontGroup = True items.append(genre_item) category_items = { "Drama": ("drama", "https://www.svtstatic.se/play/play5/images/categories/posters/drama-d75cd2da2eecde36b3d60fad6b92ad42.jpg" ), "Dokumentär": ("dokumentar", "https://www.svtstatic.se/play/play5/images/categories/posters/dokumentar-00599af62aa8009dbc13577eff894b8e.jpg" ), "Humor": ("humor", "https://www.svtstatic.se/play/play5/images/categories/posters/humor-abc329317eedf789d2cca76151213188.jpg" ), "Livsstil": ("livsstil", "https://www.svtstatic.se/play/play5/images/categories/posters/livsstil-2d9cd77d86c086fb8908ce4905b488b7.jpg" ), "Underhållning": ("underhallning", "https://www.svtstatic.se/play/play5/images/categories/posters/underhallning-a60da5125e715d74500a200bd4416841.jpg" ), "Kultur": ("kultur", "https://www.svtstatic.se/play/play5/images/categories/posters/kultur-93dca50ed1d6f25d316ac1621393851a.jpg" ), "Samhälle & Fakta": ("samhalle-och-fakta", "https://www.svtstatic.se/play/play5/images/categories/posters/samhalle-och-fakta-3750657f72529a572f3698e01452f348.jpg" ), "Film": ("film", "https://www.svtstatic.se/image/medium/480/20888292/1548755428"), "Barn": ("barn", "https://www.svtstatic.se/play/play5/images/categories/posters/barn-c17302a6f7a9a458e0043b58bbe8ab79.jpg" ), "Nyheter": ("nyheter", "https://www.svtstatic.se/play/play6/images/categories/posters/nyheter.e67ff1b5770152af4690ad188546f9e9.jpg" ), "Sport": ("sport", "https://www.svtstatic.se/play/play6/images/categories/posters/sport.98b65f6627e4addbc4177542035ea504.jpg" ) } category_title = "\a.: {} :.".format( LanguageHelper.get_localized_string(LanguageHelper.Categories)) new_item = MediaItem(category_title, "https://www.svtplay.se/genre") new_item.complete = True new_item.thumb = self.noImage new_item.dontGroup = True for title, (category_id, thumb) in category_items.items(): # https://api.svt.se/contento/graphql?ua=svtplaywebb-play-render-prod-client&operationName=GenreProgramsAO&variables={"genre": ["action-och-aventyr"]}&extensions={"persistedQuery": {"version": 1, "sha256Hash": "189b3613ec93e869feace9a379cca47d8b68b97b3f53c04163769dcffa509318"}} cat_item = MediaItem(title, "#genre_item") cat_item.complete = True cat_item.thumb = thumb or self.noImage cat_item.dontGroup = True cat_item.metaData[self.__genre_id] = category_id new_item.items.append(cat_item) items.append(new_item) return data, items
def __update_title_and_description_with_limitations(self): """ Updates the title/name and description with the symbols for DRM, GEO and Paid. :return: (tuple) name postfix, description postfix :rtype: tuple[str,str] """ geo_lock = "º" # º drm_lock = "^" # ^ paid = "ª" # ª cloaked = "¨" # ¨ description_prefix = [] title_postfix = [] description = "" title = "" if self.__expires_datetime is not None: expires = "{}: {}".format( MediaItem.ExpiresAt, self.__expires_datetime.strftime("%Y-%m-%d %H:%M")) description_prefix.append(("gold", expires)) if self.isDrmProtected: title_postfix.append(("gold", drm_lock)) description_prefix.append(("gold", LanguageHelper.get_localized_string( LanguageHelper.DrmProtected))) if self.isGeoLocked: title_postfix.append(("aqua", geo_lock)) description_prefix.append(("aqua", LanguageHelper.get_localized_string( LanguageHelper.GeoLockedId))) if self.isPaid: title_postfix.append(("gold", paid)) description_prefix.append(("gold", LanguageHelper.get_localized_string( LanguageHelper.PremiumPaid))) if self.isCloaked: title_postfix.append(("gold", cloaked)) description_prefix.append(("gold", LanguageHelper.get_localized_string( LanguageHelper.HiddenItem))) if self.uses_external_addon: from resources.lib.xbmcwrapper import XbmcWrapper external = XbmcWrapper.get_external_add_on_label(self.url) title_postfix.append(("gold", external)) def __color_text(texts, text_format="[COLOR {}]{}[/COLOR]"): """ :param list[tuple[str, str]] texts: The color and text (in tuple) :param str text_format: The format used for filling :return: A Kodi compatible color coded string. :rtype: str See https://forum.kodi.tv/showthread.php?tid=210837 """ return "".join([ text_format.format(clr, text.lstrip()) for clr, text in texts ]).strip() # actually update it if description_prefix: description = __color_text(description_prefix, text_format="[COLOR {}]{}[/COLOR]\n") if title_postfix: title = __color_text(title_postfix) return title, description
def change_pin(self, application_key=None): """ Stores an existing ApplicationKey using a new PIN. :param bytes application_key: an existing ApplicationKey that will be stored. If none specified, the existing ApplicationKey of the Vault will be used. :return: Indication of success. :rtype: bool """ Logger.info("Updating the ApplicationKey with a new PIN") if self.__newKeyGeneratedInConstructor: Logger.info("A key was just generated, no need to change PINs.") return True if application_key is None: Logger.debug("Using the ApplicationKey from the vault.") application_key = Vault.__Key else: Logger.debug("Using the ApplicationKey from the input parameter.") if not application_key: raise ValueError("No ApplicationKey specified.") # Now we get a new PIN and (re)encrypt pin = XbmcWrapper.show_key_board( heading=LanguageHelper.get_localized_string( LanguageHelper.VaultNewPin), hidden=True) if not pin: XbmcWrapper.show_notification( "", LanguageHelper.get_localized_string(LanguageHelper.VaultNoPin), XbmcWrapper.Error) return False pin2 = XbmcWrapper.show_key_board( heading=LanguageHelper.get_localized_string( LanguageHelper.VaultRepeatPin), hidden=True) if pin != pin2: Logger.critical("Mismatch in PINs") XbmcWrapper.show_notification( "", LanguageHelper.get_localized_string( LanguageHelper.VaultPinsDontMatch), XbmcWrapper.Error) return False if PY2: encrypted_key = "%s=%s" % (self.__APPLICATION_KEY_SETTING, application_key) else: # make it text to store encrypted_key = "%s=%s" % (self.__APPLICATION_KEY_SETTING, application_key.decode()) # let's generate a pin using the scrypt password-based key derivation pin_key = self.__get_pbk(pin) encrypted_key = self.__encrypt(encrypted_key, pin_key) AddonSettings.set_setting(Vault.__APPLICATION_KEY_SETTING, encrypted_key, store=LOCAL) Logger.info("Successfully updated the Retrospect PIN") return True
def load_programs(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 = [] # fetch al pages p = 1 url_format = "https://{0}/content/shows?" \ "include=images" \ "&page%5Bsize%5D=100&page%5Bnumber%5D={{0}}".format(self.baseUrlApi) # "include=images%2CprimaryChannel" \ url = url_format.format(p) data = UriHandler.open(url) json = JsonHelper(data) pages = json.get_value("meta", "totalPages") programs = json.get_value("data") or [] # extract the images self.__update_image_lookup(json) for p in range(2, pages + 1, 1): url = url_format.format(p) Logger.debug("Loading: %s", url) data = UriHandler.open(url) json = JsonHelper(data) programs += json.get_value("data") or [] # extract the images self.__update_image_lookup(json) Logger.debug("Found a total of %s items over %s pages", len(programs), pages) for p in programs: item = self.create_program_item(p) if item is not None: items.append(item) if self.recentUrl: recent_text = LanguageHelper.get_localized_string( LanguageHelper.Recent) recent = MediaItem("\b.: {} :.".format(recent_text), self.recentUrl) recent.dontGroup = True items.append(recent) # live items if self.liveUrl: live = MediaItem("\b.: Live :.", self.liveUrl) live.type = "video" live.dontGroup = True live.isGeoLocked = True live.isLive = True items.append(live) search = MediaItem("\a.: Sök :.", "searchSite") search.type = "folder" search.dontGroup = True items.append(search) return data, items
def add_categories_and_specials(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]] """ Logger.info("Performing Pre-Processing") items = [] extras = { LanguageHelper.get_localized_string(LanguageHelper.Search): ("searchSite", None, False), LanguageHelper.get_localized_string(LanguageHelper.TvShows): ("https://api.tv4play.se/play/programs?is_active=true&platform=tablet" "&per_page=1000&fl=nid,name,program_image,is_premium,updated_at,channel&start=0", None, False) } # Channel 4 specific items if self.channelCode == "tv4se": extras.update({ LanguageHelper.get_localized_string(LanguageHelper.Categories): ("https://api.tv4play.se/play/categories.json", None, False), LanguageHelper.get_localized_string(LanguageHelper.MostViewedEpisodes): ("https://api.tv4play.se/play/video_assets/most_viewed?type=episode" "&platform=tablet&is_live=false&per_page=%s&start=0" % (self.maxPageSize, ), None, False), }) today = datetime.datetime.now() days = [ LanguageHelper.get_localized_string(LanguageHelper.Monday), LanguageHelper.get_localized_string(LanguageHelper.Tuesday), LanguageHelper.get_localized_string(LanguageHelper.Wednesday), LanguageHelper.get_localized_string(LanguageHelper.Thursday), LanguageHelper.get_localized_string(LanguageHelper.Friday), LanguageHelper.get_localized_string(LanguageHelper.Saturday), LanguageHelper.get_localized_string(LanguageHelper.Sunday) ] for i in range(0, 7, 1): start_date = today - datetime.timedelta(i) end_date = start_date + datetime.timedelta(1) day = days[start_date.weekday()] if i == 0: day = LanguageHelper.get_localized_string( LanguageHelper.Today) elif i == 1: day = LanguageHelper.get_localized_string( LanguageHelper.Yesterday) Logger.trace("Adding item for: %s - %s", start_date, end_date) # Old URL: # url = "https://api.tv4play.se/play/video_assets?exclude_node_nids=" \ # "nyheterna,v%C3%A4der,ekonomi,lotto,sporten,nyheterna-blekinge,nyheterna-bor%C3%A5s," \ # "nyheterna-dalarna,nyheterna-g%C3%A4vle,nyheterna-g%C3%B6teborg,nyheterna-halland," \ # "nyheterna-helsingborg,nyheterna-j%C3%B6nk%C3%B6ping,nyheterna-kalmar,nyheterna-link%C3%B6ping," \ # "nyheterna-lule%C3%A5,nyheterna-malm%C3%B6,nyheterna-norrk%C3%B6ping,nyheterna-skaraborg," \ # "nyheterna-skellefte%C3%A5,nyheterna-stockholm,nyheterna-sundsvall,nyheterna-ume%C3%A5," \ # "nyheterna-uppsala,nyheterna-v%C3%A4rmland,nyheterna-v%C3%A4st,nyheterna-v%C3%A4ster%C3%A5s," \ # "nyheterna-v%C3%A4xj%C3%B6,nyheterna-%C3%B6rebro,nyheterna-%C3%B6stersund,tv4-tolken," \ # "fotbollskanalen-europa" \ # "&platform=tablet&per_page=32&is_live=false&product_groups=2&type=episode&per_page=100" url = "https://api.tv4play.se/play/video_assets?exclude_node_nids=" \ "&platform=tablet&per_page=32&is_live=false&product_groups=2&type=episode&per_page=100" url = "%s&broadcast_from=%s&broadcast_to=%s&" % ( url, start_date.strftime("%Y%m%d"), end_date.strftime("%Y%m%d")) extras[day] = (url, start_date, False) extras[LanguageHelper.get_localized_string( LanguageHelper.CurrentlyPlayingEpisodes )] = ( "https://api.tv4play.se/play/video_assets?exclude_node_nids=&platform=tablet&" "per_page=32&is_live=true&product_groups=2&type=episode&per_page=100", None, False) for name in extras: title = name url, date, is_live = extras[name] item = MediaItem(title, url) item.dontGroup = True item.complete = True item.thumb = self.noImage item.HttpHeaders = self.httpHeaders item.isLive = is_live if date is not None: item.set_date(date.year, date.month, date.day, 0, 0, 0, text=date.strftime("%Y-%m-%d")) items.append(item) if not self.channelCode == "tv4se": return data, items # Add Live TV # live = MediaItem("\a.: Live-TV :.", # "http://tv4events1-lh.akamaihd.net/i/EXTRAEVENT5_1@324055/master.m3u8", # type="video") # live.dontGroup = True # # live.isDrmProtected = True # live.isGeoLocked = True # live.isLive = True # items.append(live) Logger.debug("Pre-Processing finished") return data, items
def __update_embedded_video(self, item): """ Updates video items that are encrypted. This could be the default for Krypton! :param MediaItem item: The item to update. :return: An updated item. :rtype: MediaItem """ data = UriHandler.open(item.url, proxy=self.proxy) start_needle = "var playerConfig =" start_data = data.index(start_needle) + len(start_needle) end_data = data.index("var talpaPlayer") data = data[start_data:end_data].strip().rstrip(";") json = JsonHelper(data) has_drm_only = True adaptive_available = AddonSettings.use_adaptive_stream_add_on( with_encryption=False, channel=self) adaptive_available_encrypted = AddonSettings.use_adaptive_stream_add_on( with_encryption=True, channel=self) for play_list_entry in json.get_value("playlist"): part = item.create_new_empty_media_part() for source in play_list_entry["sources"]: stream_type = source["type"] stream_url = source["file"] stream_drm = source.get("drm") if not stream_drm: has_drm_only = False if stream_type == "m3u8": Logger.debug("Found non-encrypted M3u8 stream: %s", stream_url) M3u8.update_part_with_m3u8_streams(part, stream_url, proxy=self.proxy, channel=self) item.complete = True elif stream_type == "dash" and adaptive_available: Logger.debug("Found non-encrypted Dash stream: %s", stream_url) stream = part.append_media_stream(stream_url, 1) Mpd.set_input_stream_addon_input(stream, proxy=self.proxy) item.complete = True else: Logger.debug("Unknown stream source: %s", source) else: compatible_drm = "widevine" if compatible_drm not in stream_drm or stream_type != "dash": Logger.debug("Found encrypted %s stream: %s", stream_type, stream_url) continue Logger.debug("Found Widevine encrypted Dash stream: %s", stream_url) license_url = stream_drm[compatible_drm]["url"] pid = stream_drm[compatible_drm]["releasePid"] encryption_json = '{"getRawWidevineLicense":' \ '{"releasePid":"%s", "widevineChallenge":"b{SSM}"}' \ '}' % (pid,) headers = { "Content-Type": "application/json", "Origin": "https://embed.kijk.nl", "Referer": stream_url } encryption_key = Mpd.get_license_key( license_url, key_type=None, key_value=encryption_json, key_headers=headers) stream = part.append_media_stream(stream_url, 0) Mpd.set_input_stream_addon_input( stream, proxy=self.proxy, license_key=encryption_key) item.complete = True subs = [ s['file'] for s in play_list_entry.get("tracks", []) if s.get('kind') == "captions" ] if subs: subtitle = SubtitleHelper.download_subtitle(subs[0], format="webvtt") part.Subtitle = subtitle if has_drm_only and not adaptive_available_encrypted: XbmcWrapper.show_dialog( LanguageHelper.get_localized_string(LanguageHelper.DrmTitle), LanguageHelper.get_localized_string( LanguageHelper.WidevineLeiaRequired)) return item
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 = [] for media_item in media_items: # type: MediaItem self.__update_artwork(media_item, self.__channel) 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) # set the content. It needs to be "episodes" to make the MediaItem.set_season_info() work xbmcplugin.setContent(handle=self.handle, content="episodes") xbmcplugin.endOfDirectory(self.handle, ok) except Exception: Logger.error("Plugin::Error Processing FolderList", exc_info=True) XbmcWrapper.show_notification( LanguageHelper.get_localized_string(LanguageHelper.ErrorId), LanguageHelper.get_localized_string(LanguageHelper.ErrorList), XbmcWrapper.Error, 4000) xbmcplugin.endOfDirectory(self.handle, False)
def __init__(self, channel_info): """ Initialisation of the class. All class variables should be instantiated here and this method should not be overridden by any derived classes. :param ChannelInfo channel_info: The channel info object to base this channel on. """ chn_class.Channel.__init__(self, channel_info) # ============== Actual channel setup STARTS here and should be overwritten from derived classes =============== self.__channelId = "tv4" if self.channelCode == "tv4se": self.noImage = "tv4image.png" self.__channelId = "tv4" elif self.channelCode == "tv7se": self.noImage = "tv7image.png" self.__channelId = "sjuan" elif self.channelCode == "tv12se": self.noImage = "tv12image.png" self.__channelId = "tv12" else: raise Exception("Invalid channel code") # setup the urls # self.mainListUri = "https://api.tv4play.se/play/programs?is_active=true&platform=tablet&per_page=1000" \ # "&fl=nid,name,program_image&start=0" self.mainListUri = "#mainlisting" self.baseUrl = "http://www.tv4play.se" self.swfUrl = "http://www.tv4play.se/flash/tv4playflashlets.swf" self._add_data_parser(self.mainListUri, preprocessor=self.add_categories_and_specials) self.episodeItemJson = [ "results", ] self._add_data_parser( "https://api.tv4play.se/play/programs?", json=True, # No longer used: requiresLogon=True, parser=self.episodeItemJson, creator=self.create_episode_item) self._add_data_parser("https://api.tv4play.se/play/categories.json", json=True, match_type=ParserData.MatchExact, parser=[], creator=self.create_category_item) self._add_data_parser( "https://api.tv4play.se/play/programs?platform=tablet&category=", json=True, parser=self.episodeItemJson, creator=self.create_episode_item) self._add_data_parser("http://tv4live-i.akamaihd.net/hls/live/", updater=self.update_live_item) self._add_data_parser( "http://tv4events1-lh.akamaihd.net/i/EXTRAEVENT5_1", updater=self.update_live_item) self.videoItemJson = [ "results", ] self._add_data_parser("*", preprocessor=self.pre_process_folder_list, json=True, parser=self.videoItemJson, creator=self.create_video_item, updater=self.update_video_item) #=============================================================================================================== # non standard items self.maxPageSize = 25 # The Android app uses a page size of 20 self.__expires_text = LanguageHelper.get_localized_string( LanguageHelper.ExpiresAt) #=============================================================================================================== # Test cases: # Batman - WideVine # Antikdeckarna - Clips # ====================================== Actual channel setup STOPS here ======================================= return
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 selected_item = None if self.keywordPickle in self.params: selected_item = self._pickler.de_pickle_media_item(self.params[self.keywordPickle]) 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) # 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 selected_item is None and self.channelObject is not None: # mainlist item register channel. watcher.lap("Statistics send") watcher.stop() self.__add_sort_method_to_handle(self.handle, media_items) self.__add_breadcrumb(self.handle, self.channelObject, selected_item) # set the content. It needs to be "episodes" to make the MediaItem.set_season_info() work xbmcplugin.setContent(handle=self.handle, content="episodes") xbmcplugin.endOfDirectory(self.handle, ok) except Exception: Logger.error("Plugin::Error Processing FolderList", exc_info=True) XbmcWrapper.show_notification(LanguageHelper.get_localized_string(LanguageHelper.ErrorId), LanguageHelper.get_localized_string(LanguageHelper.ErrorList), XbmcWrapper.Error, 4000) xbmcplugin.endOfDirectory(self.handle, False)
def __init__(self, addon_name, params, handle=0): # NOSONAR complexity """ Initialises the plugin with given arguments. :param str addon_name: The add-on name. :param str params: The input parameters from the query string. :param int handle: The Kodi directory handle. """ Logger.info("******** Starting %s add-on version %s/repo *********", Config.appName, Config.version) # noinspection PyTypeChecker self.handle = int(handle) super(Plugin, self).__init__(addon_name, params) Logger.debug("Plugin Params: %s (%s)\n" "Handle: %s\n" "Name: %s\n" "Query: %s", self.params, len(self.params), self.handle, self.pluginName, params) # 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) # 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 extract_hero_data(self, data): """ Extacts the Hero json data 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[JsonHelper,list[MediaItem]] """ Logger.info("Performing Pre-Processing") items = [] hero_data = Regexer.do_regex(r'data-hero="([^"]+)', data)[0] hero_data = HtmlEntityHelper.convert_html_entities(hero_data) Logger.trace(hero_data) hero_json = JsonHelper(hero_data) hero_playlists = hero_json.get_value("data", "playlists") if not hero_playlists: # set an empty object hero_json.json = {} current = self.parentItem.metaData.get("current_playlist", None) if current == "clips": Logger.debug("Found 'clips' metadata, only listing clips") hero_json.json = {} return hero_json, items if current is None: # Add clips folder clip_title = LanguageHelper.get_localized_string(LanguageHelper.Clips) clips = MediaItem("\a.: %s :." % (clip_title,), self.parentItem.url) clips.metaData[self.__meta_playlist] = "clips" self.__no_clips = True items.append(clips) # See if there are seasons to show if len(hero_playlists) == 1: # first items, list all, except if there is only a single season Logger.debug("Only one folder playlist found. Listing that one") return hero_json, items if current is None: # list all folders for playlist in hero_playlists: folder = self.create_folder_item(playlist) items.append(folder) # clear the json item to prevent further listing hero_json.json = {} return hero_json, items # list the correct folder current_list = [lst for lst in hero_playlists if lst["id"] == current] if current_list: # we are listing a subfolder, put that one on index 0 and then also hero_playlists.insert(0, current_list[0]) self.__no_clips = True Logger.debug("Pre-Processing finished") return hero_json, items
def add_categories_and_specials(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]] """ Logger.info("Performing Pre-Processing") items = [] # TV4 Group specific items query = 'query{programSearch(per_page:1000){__typename,programs' \ '%s,' \ 'totalHits}}' % (self.__program_fields,) query = HtmlEntityHelper.url_encode(query) tv_shows_url = "https://graphql.tv4play.se/graphql?query={}".format( query) extras = { LanguageHelper.get_localized_string(LanguageHelper.Search): ("searchSite", None, False), LanguageHelper.get_localized_string(LanguageHelper.TvShows): (tv_shows_url, None, False), LanguageHelper.get_localized_string(LanguageHelper.Categories): ("https://graphql.tv4play.se/graphql?query=query%7Btags%7D", None, False), LanguageHelper.get_localized_string(LanguageHelper.MostViewedEpisodes): ("https://api.tv4play.se/play/video_assets/most_viewed?type=episode" "&platform=tablet&is_live=false&per_page=%s&start=0" % (self.__maxPageSize, ), None, False), } today = datetime.datetime.now() days = [ LanguageHelper.get_localized_string(LanguageHelper.Monday), LanguageHelper.get_localized_string(LanguageHelper.Tuesday), LanguageHelper.get_localized_string(LanguageHelper.Wednesday), LanguageHelper.get_localized_string(LanguageHelper.Thursday), LanguageHelper.get_localized_string(LanguageHelper.Friday), LanguageHelper.get_localized_string(LanguageHelper.Saturday), LanguageHelper.get_localized_string(LanguageHelper.Sunday) ] for i in range(0, 7, 1): start_date = today - datetime.timedelta(i) end_date = start_date + datetime.timedelta(1) day = days[start_date.weekday()] if i == 0: day = LanguageHelper.get_localized_string(LanguageHelper.Today) elif i == 1: day = LanguageHelper.get_localized_string( LanguageHelper.Yesterday) Logger.trace("Adding item for: %s - %s", start_date, end_date) url = "https://api.tv4play.se/play/video_assets?exclude_node_nids=" \ "&platform=tablet&is_live=false&product_groups=2&type=episode&per_page=100" url = "%s&broadcast_from=%s&broadcast_to=%s&" % ( url, start_date.strftime("%Y%m%d"), end_date.strftime("%Y%m%d")) extras[day] = (url, start_date, False) extras[LanguageHelper.get_localized_string( LanguageHelper.CurrentlyPlayingEpisodes )] = ( "https://api.tv4play.se/play/video_assets?exclude_node_nids=&platform=tablet&" "is_live=true&product_groups=2&type=episode&per_page=100", None, False) # Actually add the extra items for name in extras: title = name url, date, is_live = extras[name] item = MediaItem(title, url) item.dontGroup = True item.complete = True item.HttpHeaders = self.httpHeaders item.isLive = is_live if date is not None: item.set_date(date.year, date.month, date.day, 0, 0, 0, text=date.strftime("%Y-%m-%d")) items.append(item) Logger.debug("Pre-Processing finished") return data, items
def show_notification(title, lines, notification_type=Info, display_time=1500, fallback=True, logger=None): """ Shows an Kodi Notification :param str|int|None title: The title to show or its language ID. :param str|int|list[str] lines: The content to show or its language ID. :param str notification_type: The type of notification: info, error, warning. :param int display_time: Time to display the notification. Defaults to 1500 ms. :param bool fallback: Should we fallback on XbmcWrapper.show_dialog on error? :param any logger: A possible `Logger` object. """ if isinstance(title, int): title = LanguageHelper.get_localized_string(title) # check for a title if title: notification_title = "%s - %s" % (Config.appName, title) else: notification_title = Config.appName # check for content and merge multiple lines. This is to stay compatible # with the LanguageHelper.get_localized_string that returns strings as arrays # if they are multiple lines (this is because XbmcWrapper.show_dialog needs # this for multi-line dialog boxes. if not lines: notification_content = "" else: if isinstance(lines, int): notification_content = LanguageHelper.get_localized_string( lines) elif isinstance(lines, (tuple, list)): notification_content = " ".join(lines) else: notification_content = lines # determine the duration notification_type = notification_type.lower() if notification_type == XbmcWrapper.Warning and display_time < 2500: display_time = 2500 elif notification_type == XbmcWrapper.Info and display_time < 5000: display_time = 5000 elif display_time < 1500: # cannot be smaller then 1500 (API limit) display_time = 1500 # Get an icon notification_icon = Config.icon if os.path.exists(notification_icon): # change the separators notification_icon = notification_icon.replace("\\", "/") else: notification_icon = notification_type if notification_type not in AddonSettings.get_notification_level(): return if logger: logger.debug("Showing notification: %s - %s", notification_title, notification_content) try: xbmcgui.Dialog().notification(notification_title, notification_content, icon=notification_icon, time=display_time) return except: if fallback: XbmcWrapper.show_dialog(title or "", lines or "") # no reason to worry if this does not work on older XBMC's return
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|unicode 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]] """ Logger.info("Performing Pre-Processing") items = [] # Add a klip folder only on the first page and only if it is not already a clip page if "type=clip" not in self.parentItem.url \ and "&page=1&" in self.parentItem.url \ and "node_nids=" in self.parentItem.url: # get the category ID cat_start = self.parentItem.url.rfind("node_nids=") cat_id = self.parentItem.url[cat_start + 10:] Logger.debug("Currently doing CatId: '%s'", cat_id) url = "https://api.tv4play.se/play/video_assets?platform=tablet&per_page=%s&" \ "type=clip&page=1&node_nids=%s&start=0" % (self.__maxPageSize, cat_id,) clips_title = LanguageHelper.get_localized_string( LanguageHelper.Clips) clips = MediaItem(clips_title, url) clips.complete = True items.append(clips) # find the max number of items ("total_hits":2724) total_items = int(Regexer.do_regex(r'total_hits\W+(\d+)', data)[-1]) Logger.debug("Found total of %s items. Only showing %s.", total_items, self.__maxPageSize) if total_items > self.__maxPageSize and "&page=1&" in self.parentItem.url: # create a group item more_title = LanguageHelper.get_localized_string( LanguageHelper.MorePages) more = MediaItem(more_title, "") more.complete = True items.append(more) # what are the total number of pages? current_page = 1 # noinspection PyTypeChecker total_pages = int(math.ceil(1.0 * total_items / self.__maxPageSize)) current_url = self.parentItem.url needle = "&page=" while current_page < total_pages: # what is the current page current_page += 1 url = current_url.replace("%s1" % (needle, ), "%s%s" % (needle, current_page)) Logger.debug("Adding next page: %s\n%s", current_page, url) page = MediaItem(str(current_page), url) page.type = "page" page.complete = True if total_pages == 2: items = [page] break else: more.items.append(page) Logger.debug("Pre-Processing finished") return data, items
def __init__(self, channel_info): """ Initialisation of the class. All class variables should be instantiated here and this method should not be overridden by any derived classes. :param ChannelInfo channel_info: The channel info object to base this channel on. """ chn_class.Channel.__init__(self, channel_info) # ==== Actual channel setup STARTS here and should be overwritten from derived classes ==== self.noImage = "urplayimage.png" # setup the urls self.mainListUri = "https://urplay.se/api/bff/v1/search?product_type=series&rows=10000&start=0" self.baseUrl = "https://urplay.se" self.swfUrl = "https://urplay.se/assets/jwplayer-6.12-17973009ab259c1dea1258b04bde6e53.swf" # Match the "series" API -> shows TV Shows self._add_data_parser(self.mainListUri, json=True, name="Show parser with categories", match_type=ParserData.MatchExact, preprocessor=self.add_categories_and_search, parser=["results"], creator=self.create_episode_json_item) # Match Videos (programs) self._add_data_parser( "https://urplay.se/api/bff/v1/search?product_type=program", name="Most viewed", json=True, parser=["results"], creator=self.create_video_item_json) self._add_data_parser("*", json=True, name="Json based video parser", preprocessor=self.extract_json_data, parser=["currentProduct", "series", "programs"], creator=self.create_video_item_json) self._add_data_parser("*", updater=self.update_video_item) # Categories cat_reg = r'<a[^>]+href="(?<url>/blad[^"]+/(?<slug>[^"]+))"[^>]*>(?<title>[^<]+)<' cat_reg = Regexer.from_expresso(cat_reg) self._add_data_parser("https://urplay.se/", name="Category parser", match_type=ParserData.MatchExact, parser=cat_reg, creator=self.create_category_item) self._add_data_parsers([ "https://urplay.se/api/bff/v1/search?play_category", "https://urplay.se/api/bff/v1/search?response_type=category" ], name="Category content", json=True, parser=["results"], creator=self.create_json_item) # Searching self._add_data_parser("https://urplay.se/search/json", json=True, parser=["programs"], creator=self.create_search_result_program) self._add_data_parser("https://urplay.se/search/json", json=True, parser=["series"], creator=self.create_search_result_serie) self.mediaUrlRegex = r"urPlayer.init\(([^<]+)\);" #=========================================================================================== # non standard items self.__videoItemFound = False self.__cateogory_slugs = { "dokumentarfilmer": "dokument%C3%A4rfilmer", "forelasningar": "f%C3%B6rel%C3%A4sningar", "kultur-och-historia": "kultur%20och%20historia", "reality-och-livsstil": "reality%20och%20livsstil", "samhalle": "samh%C3%A4lle" } self.__cateogory_urls = { "radio": "https://urplay.se/api/bff/v1/search?response_type=category" "&singles_and_series=true" "&rows=1000&start=0" "&type=programradio&view=title", "syntolkat": "https://urplay.se/api/bff/v1/search?response_type=category" "&singles_and_series=true" "&rows=1000&start=0" "&view=title" "&with_audio_description=true", "teckensprak": "https://urplay.se/api/bff/v1/search?response_type=category" "&language=sgn-SWE" "&rows=1000&start=0" "&view=title" "&singles_and_series=true&view=title" } self.__timezone = pytz.timezone("Europe/Amsterdam") self.__episode_text = LanguageHelper.get_localized_string( LanguageHelper.EpisodeId) #=========================================================================================== # Test cases: # Anaconda Auf Deutch : RTMP, Subtitles # ====================================== Actual channel setup STOPS here =================== return
def execute(self): 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.__channel.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.__channel)) 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 process_live_items(self, data): # NOSONAR """ Performs pre-process actions that either return multiple live channels that are present in the live url or an actual list item if a single live stream is present. Accepts an data from the process_folder_list method, BEFORE the items are processed. Allows setting of parameters (like title etc) for the channel. Inside this method the <data> could be changed and additional items can be created. The return values should always be instantiated in at least ("", []). :param str data: The retrieve data that was loaded for the current item and URL. :return: A tuple of the data and a list of MediaItems that were generated. :rtype: tuple[str|JsonHelper,list[MediaItem]] """ items = [] Logger.info("Adding Live Streams") if self.liveUrl.endswith(".m3u8"): # We actually have a single stream. title = "{} - {}".format( self.channelName, LanguageHelper.get_localized_string( LanguageHelper.LiveStreamTitleId)) live_item = MediaItem(title, self.liveUrl) live_item.type = 'video' live_item.isLive = True if self.channelCode == "rtvdrenthe": # RTV Drenthe actually has a buggy M3u8 without master index. live_item.append_single_stream(live_item.url, 0) live_item.complete = True items.append(live_item) return "", items # we basically will check for live channels json_data = JsonHelper(data, logger=Logger.instance()) live_streams = json_data.get_value() Logger.trace(live_streams) if "videos" in live_streams: Logger.debug("Multiple streams found") live_streams = live_streams["videos"] elif not isinstance(live_streams, (list, tuple)): Logger.debug("Single streams found") live_streams = (live_streams, ) else: Logger.debug("List of stream found") live_stream_value = None for streams in live_streams: Logger.debug("Adding live stream") title = streams.get( 'name') or "%s - Live TV" % (self.channelName, ) live_item = MediaItem(title, self.liveUrl) live_item.type = 'video' live_item.complete = True live_item.isLive = True part = live_item.create_new_empty_media_part() for stream in streams: Logger.trace(stream) bitrate = None # used in Omrop Fryslan if stream == "android" or stream == "iPhone": bitrate = 250 url = streams[stream]["videoLink"] elif stream == "iPad": bitrate = 1000 url = streams[stream]["videoLink"] # used in RTV Utrecht elif stream == "androidLink" or stream == "iphoneLink": bitrate = 250 url = streams[stream] elif stream == "ipadLink": bitrate = 1000 url = streams[stream] elif stream == "tabletLink": bitrate = 300 url = streams[stream] # These windows stream won't work # elif stream == "windowsLink": # bitrate = 1200 # url = streams[stream] # elif stream == "wpLink": # bitrate = 1200 # url = streams[stream] elif stream == "name": Logger.trace("Ignoring stream '%s'", stream) else: Logger.warning("No url found for type '%s'", stream) # noinspection PyUnboundLocalVariable if "livestreams.omroep.nl/live/" in url and url.endswith( "m3u8"): Logger.info("Found NPO Stream, adding ?protection=url") url = "%s?protection=url" % (url, ) if bitrate: part.append_media_stream(url, bitrate) if url == live_stream_value and ".m3u8" in url: # if it was equal to the previous one, assume we have a m3u8. Reset the others. Logger.info( "Found same M3u8 stream for all streams for this Live channel, using that one: %s", url) live_item.MediaItemParts = [] live_item.url = url live_item.complete = False break elif "playlist.m3u8" in url: # if we have a playlist, use that one. Reset the others. Logger.info( "Found M3u8 playlist for this Live channel, using that one: %s", url) live_item.MediaItemParts = [] live_item.url = url live_item.complete = False break else: # add it to the possibilities live_stream_value = url items.append(live_item) return "", items
def execute(self): if self.category: Logger.info("Plugin::show_channel_list for %s", self.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.parameter_parser.create_action_url( None, action=action.ALL_FAVOURITES) xbmc_items.append((url, kodi_item, True)) for channel in channels: if self.category and channel.category != self.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.parameter_parser.create_action_url( channel, action=action.LIST_FOLDER) # 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 _get_context_menu_items(self, channel, item=None): """ Retrieves the custom context menu items to display. :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 = [] # Generic, 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, '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.parameter_parser.create_action_url( channel, action=menu_item.functionName, item=item) cmd = "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 __init__(self, channel_info): """ Initialisation of the class. All class variables should be instantiated here and this method should not be overridden by any derived classes. :param ChannelInfo channel_info: The channel info object to base this channel on. """ chn_class.Channel.__init__(self, channel_info) # ============== Actual channel setup STARTS here and should be overwritten from derived classes =============== # The following data was taken from http://playapi.mtgx.tv/v3/channels self.channelId = None if self.channelCode == "se3": self.mainListUri = "https://www.viafree.se/program/" self.noImage = "tv3seimage.png" self.channelId = ( 1209, # TV4 6000, # MTV 6001, # Comedy Central 7000, # Online Only ??? ) elif self.channelCode == "se6": self.mainListUri = "https://www.viafree.se/program/" self.noImage = "tv6seimage.png" self.channelId = (959, ) elif self.channelCode == "se8": self.mainListUri = "https://www.viafree.se/program/" self.noImage = "tv8seimage.png" self.channelId = (801, ) elif self.channelCode == "se10": self.mainListUri = "https://www.viafree.se/program/" self.noImage = "tv10seimage.png" self.channelId = (5462, ) elif self.channelCode == "ngse": self.mainListUri = "https://www.viafree.se/program/" self.noImage = "ngnoimage.jpg" self.channelId = (7300, ) elif self.channelCode == "mtvse": self.mainListUri = "https://www.viafree.se/program" self.noImage = "mtvimage.png" self.channelId = (6000, ) elif self.channelCode == "viafreese": self.mainListUri = "https://www.viafree.se/program/" self.noImage = "viafreeimage.png" self.channelId = None elif self.channelCode == "sesport": raise NotImplementedError('ViaSat sport is not in this channel anymore.') # Danish channels elif self.channelCode == "tv3dk": self.mainListUri = "http://www.viafree.dk/programmer" self.noImage = "tv3noimage.png" # self.channelId = (3687, 6200, 6201) -> show all for now # Norwegian Channels elif self.channelCode == "no3": self.mainListUri = "https://www.viafree.no/programmer" self.noImage = "tv3noimage.png" self.channelId = (1550, 6100, 6101) elif self.channelCode == "no4": self.mainListUri = "https://www.viafree.no/programmer" self.noImage = "viasat4noimage.png" self.channelId = (935,) elif self.channelCode == "no6": self.mainListUri = "https://www.viafree.no/programmer" self.noImage = "viasat4noimage.png" self.channelId = (1337,) self.baseUrl = self.mainListUri.rsplit("/", 1)[0] self.searchInfo = { "se": ["sok", "Sök"], "ee": ["otsing", "Otsi"], "dk": ["sog", "Søg"], "no": ["sok", "Søk"], "lt": ["paieska", "Paieška"], "lv": ["meklet", "Meklēt"] } # setup the urls self.swfUrl = "http://flvplayer.viastream.viasat.tv/flvplayer/play/swf/MTGXPlayer-1.8.swf" # New JSON page data self._add_data_parser(self.mainListUri, preprocessor=self.extract_json_data, match_type=ParserData.MatchExact) self._add_data_parser(self.mainListUri, preprocessor=self.extract_categories_and_add_search, json=True, match_type=ParserData.MatchExact, parser=["page", "blocks", 0, "_embedded", "programs"], creator=self.create_json_episode_item) # This is the new way, but more complex and some channels have items with missing # category slugs and is not compatible with the old method channels. self.useNewPages = False if self.useNewPages: self._add_data_parser("*", preprocessor=self.extract_json_data) self._add_data_parser("*", json=True, preprocessor=self.merge_season_data, # parser=["context", "dispatcher", "stores", "ContentPageProgramStore", "format", "videos", "0", "program"), # creator=self.create_json_video_item ) self._add_data_parser("http://playapi.mtgx.tv/", updater=self.update_video_item) else: self._add_data_parser("*", parser=['_embedded', 'videos'], json=True, preprocessor=self.add_clips, creator=self.create_video_item, updater=self.update_video_item) self.pageNavigationJson = ["_links", "next"] self.pageNavigationJsonIndex = 0 self._add_data_parser("*", json=True, parser=self.pageNavigationJson, creator=self.create_page_item) self._add_data_parser("https://playapi.mtgx.tv/v3/search?term=", json=True, parser=["_embedded", "formats"], creator=self.create_json_search_item) self._add_data_parser("/api/playClient;isColumn=true;query=", json=True, match_type=ParserData.MatchContains, parser=["data", "formats"], creator=self.create_json_episode_item) self._add_data_parser("/api/playClient;isColumn=true;query=", json=True, match_type=ParserData.MatchContains, parser=["data", "clips"], creator=self.create_json_video_item) self._add_data_parser("/api/playClient;isColumn=true;query=", json=True, match_type=ParserData.MatchContains, parser=["data", "episodes"], creator=self.create_json_video_item) # =============================================================================================================== # non standard items self.episodeLabel = LanguageHelper.get_localized_string(LanguageHelper.EpisodeId) self.seasonLabel = LanguageHelper.get_localized_string(LanguageHelper.SeasonId) self.__categories = {} # =============================================================================================================== # Test Cases # No GEO Lock: Extra Extra # GEO Lock: # Multi Bitrate: Glamourama # ====================================== Actual channel setup STOPS here ======================================= return
def add_live_items_and_genres(self, data): """ Adds the Live items, Channels and Last Episodes to the listing. :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 = [] # Specify the name, url and whether or not to filter out some subheadings: extra_items = { LanguageHelper.get_localized_string(LanguageHelper.LiveTv): ( self.__get_api_url("ChannelsQuery", "65ceeccf67cc8334bc14eb495eb921cffebf34300562900076958856e1a58d37", {}), False), LanguageHelper.get_localized_string(LanguageHelper.CurrentlyPlayingEpisodes): ( self.__get_api_url("GridPage", "265677a2465d93d39b536545cdc3664d97e3843ce5e34f145b2a45813b85007b", variables={"selectionId": "live"}), True), LanguageHelper.get_localized_string(LanguageHelper.Search): ( "searchSite", False), LanguageHelper.get_localized_string(LanguageHelper.Recent): ( self.__get_api_url("GridPage", "265677a2465d93d39b536545cdc3664d97e3843ce5e34f145b2a45813b85007b", variables={"selectionId": "latest"}), False), LanguageHelper.get_localized_string(LanguageHelper.LastChance): ( self.__get_api_url("GridPage", "265677a2465d93d39b536545cdc3664d97e3843ce5e34f145b2a45813b85007b", variables={"selectionId": "lastchance"}), False), LanguageHelper.get_localized_string(LanguageHelper.MostViewedEpisodes): ( self.__get_api_url("GridPage", "265677a2465d93d39b536545cdc3664d97e3843ce5e34f145b2a45813b85007b", variables={"selectionId": "popular"}), False) } for title, (url, include_subheading) in extra_items.items(): new_item = MediaItem("\a.: %s :." % (title, ), url) new_item.complete = True new_item.dontGroup = True new_item.metaData[self.__filter_subheading] = include_subheading items.append(new_item) genre_tags = "\a.: {}/{} :.".format( LanguageHelper.get_localized_string(LanguageHelper.Genres), LanguageHelper.get_localized_string(LanguageHelper.Tags).lower() ) genre_url = self.__get_api_url("AllGenres", "6bef51146d05b427fba78f326453127f7601188e46038c9a5c7b9c2649d4719c", {}) genre_item = MediaItem(genre_tags, genre_url) genre_item.complete = True genre_item.dontGroup = True items.append(genre_item) category_items = { "Drama": ( "drama", "https://www.svtstatic.se/image/medium/480/7166155/1458037803" ), "Dokumentär": ( "dokumentar", "https://www.svtstatic.se/image/medium/480/7166209/1458037873" ), "Humor": ( "humor", "https://www.svtstatic.se/image/medium/480/7166065/1458037609" ), "Livsstil": ( "livsstil", "https://www.svtstatic.se/image/medium/480/7166101/1458037687" ), "Underhållning": ( "underhallning", "https://www.svtstatic.se/image/medium/480/7166041/1458037574" ), "Kultur": ( "kultur", "https://www.svtstatic.se/image/medium/480/7166119/1458037729" ), "Samhälle & Fakta": ( "samhalle-och-fakta", "https://www.svtstatic.se/image/medium/480/7166173/1458037837" ), "Filmer": ( "filmer", "https://www.svtstatic.se/image/medium/480/20888292/1548755428" ), "Barn": ( "barn", "https://www.svtstatic.se/image/medium/480/22702778/1560934663" ), "Nyheter": ( "nyheter", "https://www.svtstatic.se/image/medium/480/7166089/1458037651" ), "Sport": ( "sport", "https://www.svtstatic.se/image/medium/480/7166143/1458037766" ), "Serier": ( "serier", "https://www.svtstatic.se/image/medium/480/20888260/1548755402" ), "Reality": ( "reality", "https://www.svtstatic.se/image/medium/480/21866138/1555059667" ), "Ung": ( "ung-i-play", "https://www.svtstatic.se/image/medium/480/20888300/1548755484" ), "Musik": ( "musik", "https://www.svtstatic.se/image/medium/480/19417384/1537791920" ) } category_title = "\a.: {} :.".format( LanguageHelper.get_localized_string(LanguageHelper.Categories)) new_item = MediaItem(category_title, "https://www.svtplay.se/genre") new_item.complete = True new_item.dontGroup = True for title, (category_id, thumb) in category_items.items(): # https://api.svt.se/contento/graphql?ua=svtplaywebb-play-render-prod-client&operationName=GenreProgramsAO&variables={"genre": ["action-och-aventyr"]}&extensions={"persistedQuery": {"version": 1, "sha256Hash": "189b3613ec93e869feace9a379cca47d8b68b97b3f53c04163769dcffa509318"}} cat_item = MediaItem(title, "#genre_item") cat_item.complete = True cat_item.thumb = thumb or self.noImage cat_item.fanart = thumb or self.fanart cat_item.dontGroup = True cat_item.metaData[self.__genre_id] = category_id new_item.items.append(cat_item) items.append(new_item) progs = MediaItem( LanguageHelper.get_localized_string(LanguageHelper.TvShows), self.__program_url) items.append(progs) if self.__show_program_folder: clips = MediaItem( "\a.: {} :.".format(LanguageHelper.get_localized_string(LanguageHelper.SingleEpisodes)), self.__program_url ) items.append(clips) # split the item types clips.metaData["list_type"] = "videos" progs.metaData["list_type"] = "folders" # Clean up the titles for item in items: item.name = item.name.strip("\a.: ") return data, items
def __init__(self, channel_info): """ Initialisation of the class. All class variables should be instantiated here and this method should not be overridden by any derived classes. :param ChannelInfo channel_info: The channel info object to base this channel on. """ chn_class.Channel.__init__(self, channel_info) # ============== Actual channel setup STARTS here and should be overwritten from derived classes =============== self.noImage = "svtimage.png" # Setup the urls self.mainListUri = self.__get_api_url( "ProgramsListing", "1eeb0fb08078393c17658c1a22e7eea3fbaa34bd2667cec91bbc4db8d778580f", {}) self.baseUrl = "https://www.svtplay.se" self.swfUrl = "https://media.svt.se/swf/video/svtplayer-2016.01.swf" # In case we use the All Titles and Singles with the API self._add_data_parser( "https://api.svt.se/contento/graphql?ua=svtplaywebb-play-render-prod-client&operationName=ProgramsListing", match_type=ParserData.MatchStart, json=True, preprocessor=self.add_live_items_and_genres, parser=["data", "programAtillO", "flat"], creator=self.create_api_typed_item) # This one contructs an API url using the metaData['slug'] and then retrieves either the all # folders, just the videos in a folder if that is the only folder, or it retrieves the # content of a folder with a given metaData['folder_id']. self._add_data_parser("#program_item", json=True, name="Data retriever for API folder.", preprocessor=self.fetch_program_api_data) self._add_data_parser("#program_item", json=True, name="Folder parser for show listing via API", parser=["folders"], creator=self.create_api_typed_item) self._add_data_parser("#program_item", json=True, name="video parser for show listing via API", parser=["videos"], creator=self.create_api_typed_item) self._add_data_parser( "https://api.svt.se/contento/graphql?ua=svtplaywebb-play-render-prod-client&operationName=GridPage", name="Default GraphQL GridePage parsers", json=True, parser=["data", "startForSvtPlay", "selections", 0, "items"], creator=self.create_api_typed_item) self._add_data_parser( "https://api.svt.se/contento/graphql?ua=svtplaywebb-play-render-prod-client&operationName=AllGenres", json=True, name="Genre GraphQL", parser=["data", "genresSortedByName", "genres"], creator=self.create_api_typed_item) self._add_data_parser("#genre_item", json=True, name="Genre data retriever for GraphQL", preprocessor=self.fetch_genre_api_data) self._add_data_parser("#genre_item", json=True, name="Genre episode parser for GraphQL", parser=["programs"], creator=self.create_api_typed_item) self._add_data_parser("#genre_item", json=True, name="Genre clip parser for GraphQL", parser=["videos"], creator=self.create_api_typed_item) # Setup channel listing based on JSON data in the HTML self._add_data_parser("https://www.svtplay.se/kanaler", match_type=ParserData.MatchExact, name="Live streams", json=True, preprocessor=self.extract_live_channel_data, parser=[], creator=self.create_channel_item) # Searching self._add_data_parser( "https://api.svt.se/contento/graphql?ua=svtplaywebb-play-render-prod-client&operationName=SearchPage", json=True, parser=["data", "search"], creator=self.create_api_typed_item) # Generic updating of videos self._add_data_parser("https://api.svt.se/videoplayer-api/video/", updater=self.update_video_api_item) # Update via HTML pages self._add_data_parser("https://www.svtplay.se/video/", updater=self.update_video_html_item) self._add_data_parser("https://www.svtplay.se/klipp/", updater=self.update_video_html_item) # Update via the new API urls self._add_data_parser("https://www.svt.se/videoplayer-api/", updater=self.update_video_api_item) # =============================================================================================================== # non standard items self.__folder_id = "folder_id" self.__genre_id = "genre_id" self.__parent_images = "parent_thumb_data" self.__apollo_data = None self.__expires_text = LanguageHelper.get_localized_string( LanguageHelper.ExpiresAt) self.__timezone = pytz.timezone("Europe/Stockholm") # =============================================================================================================== # Test cases: # Affaren Ramel: just 1 folder -> should only list videos # ====================================== Actual channel setup STOPS here ======================================= return
def __init__(self, addon_name, params, handle=0): """ Initialises the plugin with given arguments. :param str addon_name: The add-on name. :param str params: The input parameters from the query string. :param int|str handle: The Kodi directory handle. """ Logger.info("******** Starting %s add-on version %s/repo *********", Config.appName, Config.version) # noinspection PyTypeChecker super(Plugin, self).__init__(addon_name, handle, params) Logger.debug(self) # are we in session? session_active = SessionHelper.is_session_active(Logger.instance()) # fetch some environment settings env_ctrl = envcontroller.EnvController(Logger.instance()) if not session_active: # do add-on start stuff Logger.info("Add-On start detected. Performing startup actions.") # print the folder structure env_ctrl.print_retrospect_settings_and_folders( Config, AddonSettings) # show notification XbmcWrapper.show_notification(None, LanguageHelper.get_localized_string( LanguageHelper.StartingAddonId) % (Config.appName, ), fallback=False, logger=Logger) # check for updates. Using local import for performance from resources.lib.updater import Updater up = Updater(Config.updateUrl, Config.version, UriHandler.instance(), Logger.instance(), AddonSettings.get_release_track()) if up.is_new_version_available(): Logger.info("Found new version online: %s vs %s", up.currentVersion, up.onlineVersion) notification = LanguageHelper.get_localized_string( LanguageHelper.NewVersion2Id) notification = notification % (Config.appName, up.onlineVersion) XbmcWrapper.show_notification(None, lines=notification, display_time=20000) # check for cache folder env_ctrl.cache_check() # do some cache cleanup env_ctrl.cache_clean_up(Config.cacheDir, Config.cacheValidTime) # empty picklestore self.pickler.purge_store(Config.addonId) # create a session SessionHelper.create_session(Logger.instance())
def __init__(self, channel_info): """ Initialisation of the class. All class variables should be instantiated here and this method should not be overridden by any derived classes. :param ChannelInfo channel_info: The channel info object to base this channel on. """ chn_class.Channel.__init__(self, channel_info) # ==== Actual channel setup STARTS here and should be overwritten from derived classes ==== self.noImage = "urplayimage.png" # setup the urls self.mainListUri = "#mainlist_merge" self.baseUrl = "https://urplay.se" self.swfUrl = "https://urplay.se/assets/jwplayer-6.12-17973009ab259c1dea1258b04bde6e53.swf" # Match the "series" API -> shows TV Shows self._add_data_parser( self.mainListUri, json=True, name="Show parser with categories", match_type=ParserData.MatchExact, preprocessor=self.merge_add_categories_and_search, parser=["results"], creator=self.create_episode_json_item) # Match Videos (programs) self._add_data_parser( "https://urplay.se/api/bff/v1/search?product_type=program", name="Most viewed", json=True, parser=["results"], creator=self.create_video_item_json_with_show_title) self._add_data_parser("*", json=True, name="Json based video parser", parser=["accessibleEpisodes"], creator=self.create_video_item_json) self._add_data_parser("*", updater=self.update_video_item) # Categories cat_reg = r'<a[^>]+href="(?<url>/blad[^"]+/(?<slug>[^"]+))"[^>]*>' \ r'(?:<svg[\w\W]{0,2000}?</svg>)?(?<title>[^<]+)<' cat_reg = Regexer.from_expresso(cat_reg) self._add_data_parser("https://urplay.se/", name="Category parser", match_type=ParserData.MatchExact, parser=cat_reg, creator=self.create_category_item) self._add_data_parsers([ "https://urplay.se/api/bff/v1/search?play_category", "https://urplay.se/api/bff/v1/search?main_genre", "https://urplay.se/api/bff/v1/search?response_type=category", "https://urplay.se/api/bff/v1/search?type=programradio", "https://urplay.se/api/bff/v1/search?age=", "https://urplay.se/api/bff/v1/search?response_type=limited" ], name="Category content", json=True, preprocessor=self.merge_category_items, parser=["results"], creator=self.create_json_item) # Searching self._add_data_parser("https://urplay.se/search/json", json=True, parser=["programs"], creator=self.create_search_result_program) self._add_data_parser("https://urplay.se/search/json", json=True, parser=["series"], creator=self.create_search_result_serie) self.mediaUrlRegex = r"urPlayer.init\(([^<]+)\);" #=========================================================================================== # non standard items self.__videoItemFound = False # There is either a slug lookup or an url lookup self.__cateogory_slugs = {} self.__cateogory_urls = { "alla-program": "https://urplay.se/api/bff/v1/search?" "response_type=limited&" "product_type=series&" "rows={}&start={}&view=title", "barn": "https://urplay.se/api/bff/v1/search?" "age=children&" "platform=urplay&" "rows={}&" "singles_and_series=true&" "start={}" "&view=title", "dokumentarfilmer": "https://urplay.se/api/bff/v1/search?" "main_genre[]=dokument%C3%A4rfilm&main_genre[]=dokument%C3%A4rserie&" # "platform=urplay&" "singles_and_series=true&view=title&" "rows={}&" "singles_and_series=true&" "start={}" "&view=title", "drama": "https://urplay.se/api/bff/v1/search?" "main_genre[]=drama&main_genre[]=kortfilm&main_genre[]=fiktiva%20ber%C3%A4ttelser&" "platform=urplay&" "rows={}&" "singles_and_series=true&" "start={}&" "view=title", "forelasningar": "https://urplay.se/api/bff/v1/search?" "main_genre[]=f%C3%B6rel%C3%A4sning&main_genre[]=panelsamtal&" "platform=urplay&" "rows={}&" "singles_and_series=true&" "start={}&" "view=title", "halsa-och-relationer": "https://urplay.se/api/bff/v1/search?" "main_genre_must_not[]=forelasning&" "main_genre_must_not[]=panelsamtal&" "platform=urplay&" "rows={}&" "sab_category=kropp%20%26%20sinne&" "singles_and_series=true&" "start={}&" "view=title", "kultur-och-historia": "https://urplay.se/api/bff/v1/search?" "main_genre_must_not[]=forelasning&main_genre_must_not[]=panelsamtal&" "platform=urplay&" "rows={}&" "sab_category=kultur%20%26%20historia&" "singles_and_series=true&" "start={}&" "view=title", "natur-och-resor": "https://urplay.se/api/bff/v1/search?" "main_genre_must_not[]=forelasning&main_genre_must_not[]=panelsamtal&" "platform=urplay&" "rows={}&" "sab_category=natur%20%26%20resor&" "singles_and_series=true&" "start={}&" "view=title", "radio": "https://urplay.se/api/bff/v1/search?" "type=programradio&" "platform=urplay&" "rows={}&" "singles_and_series=true&" "start={}&" "view=title", "samhalle": "https://urplay.se/api/bff/v1/search?" "main_genre_must_not[]=forelasning&main_genre_must_not[]=panelsamtal&" "platform=urplay&" "rows={}&" "sab_category=samh%C3%A4lle&" "singles_and_series=true&" "start={}&" "view=title", "sprak": "https://urplay.se/api/bff/v1/search?" "main_genre_must_not[]=forelasning&main_genre_must_not[]=panelsamtal&" "platform=urplay&" "rows={}&" "sab_category=spr%C3%A5k&" "singles_and_series=true&" "start={}&" "view=title", "syntolkat": "https://urplay.se/api/bff/v1/search?" "response_type=category&" "is_audio_described=true&" "platform=urplay&" "rows={}&" "singles_and_series=true&" "start={}&" "view=title", "teckensprak": "https://urplay.se/api/bff/v1/search?" "response_type=category&" "language=sgn-SWE&" "platform=urplay&" "rows={}&" "singles_and_series=true&" "start={}&" "view=title", "utbildning-och-media": "https://urplay.se/api/bff/v1/search?" "main_genre_must_not[]=forelasning&" "main_genre_must_not[]=panelsamtal&" "platform=urplay&" "rows={}&" "sab_category=utbildning%20%26%20media&" "singles_and_series=true&" "start={}&" "view=title", "vetenskap": "https://urplay.se/api/bff/v1/search?" "main_genre_must_not[]=forelasning&main_genre_must_not[]=panelsamtal&" "platform=urplay&" "rows={}&" "sab_category=vetenskap%20%26%20teknik&" "singles_and_series=true&" "start={}&" "view=title" } self.__timezone = pytz.timezone("Europe/Amsterdam") self.__episode_text = LanguageHelper.get_localized_string( LanguageHelper.EpisodeId) #=========================================================================================== # Test cases: # Anaconda Auf Deutch : RTMP, Subtitles # ====================================== Actual channel setup STOPS here =================== return
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 = "\a{}".format(LanguageHelper.get_localized_string(LanguageHelper.OtherChars)) title_format = "\a{}".format(LanguageHelper.get_localized_string(LanguageHelper.StartWith)) result = dict() non_grouped = [] # Should we remove prefixes just as Kodi does? # prefixes = ("de", "het", "the", "een", "a", "an") for sub_item in items: if sub_item.dontGroup or sub_item.type != "folder": non_grouped.append(sub_item) continue char = sub_item.name[0].upper() # Should we de-prefix? # for p in prefixes: # if sub_item.name.lower().startswith(p + " "): # char = sub_item.name[len(p) + 1][0].upper() if char.isdigit(): char = "0-9" elif not char.isalpha(): char = other if char not in result: Logger.trace("Creating Grouped item from: %s", sub_item) if char == other: item = MediaItem(title_format.replace("'", "") % (char,), "") else: item = MediaItem(title_format % (char.upper(),), "") item.complete = True # item.set_date(2100 + ord(char[0]), 1, 1, text='') result[char] = item else: item = result[char] item.items.append(sub_item) items = non_grouped + list(result.values()) # In order to get a better performance in de-duplicating and keeping the sort order # we first need to store the order in a lookup table. Then we use sorted(set()) and # use that lookup table for sorting. Using sorted(set(), items.index) this will be # an O(n) (for the index()) times O(n*log(n)) (for the sorted) = O(n^2*log(n)!. # The dictionary lookup (O(1)) saves us an O(n). # See https://wiki.python.org/moin/TimeComplexity sorted_order = {} for i in range(0, len(items)): sorted_order[items[i]] = i unique_results = sorted(set(items), key=sorted_order.get) Logger.trace("Found '%d' items of which '%d' are unique.", len(items), len(unique_results)) return unique_results
def __update_title_and_description_with_limitations(self): """ Updates the title/name and description with the symbols for DRM, GEO and Paid. :return: (tuple) name postfix, description postfix :rtype: tuple[str,str] """ geo_lock = "º" # º drm_lock = "^" # ^ paid = "ª" # ª cloaked = "¨" # ¨ description_prefix = [] title_postfix = [] description = "" title = "" if self.__expires_datetime is not None: expires = "{}: {}".format( MediaItem.ExpiresAt, self.__expires_datetime.strftime("%Y-%m-%d %H:%M")) description_prefix.append(expires) if self.isDrmProtected: title_postfix.append(drm_lock) description_prefix.append( LanguageHelper.get_localized_string( LanguageHelper.DrmProtected)) if self.isGeoLocked: title_postfix.append(geo_lock) description_prefix.append( LanguageHelper.get_localized_string( LanguageHelper.GeoLockedId)) if self.isPaid: title_postfix.append(paid) description_prefix.append( LanguageHelper.get_localized_string( LanguageHelper.PremiumPaid)) if self.isCloaked: title_postfix.append(cloaked) description_prefix.append( LanguageHelper.get_localized_string(LanguageHelper.HiddenItem)) if self.uses_external_addon: from resources.lib.xbmcwrapper import XbmcWrapper external = XbmcWrapper.get_external_add_on_label(self.url) title_postfix.append(external) # actually update it if description_prefix: description_prefix = "\n".join(description_prefix) description = "[COLOR gold][I]%s[/I][/COLOR]" % ( description_prefix.rstrip(), ) if title_postfix: title = "".join(title_postfix) title = "[COLOR gold]%s[/COLOR]" % (title.lstrip(), ) return title, description
def add_categories_and_specials(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]] """ Logger.info("Performing Pre-Processing") items = [] extras = { LanguageHelper.get_localized_string(LanguageHelper.Search): ("searchSite", None, False), LanguageHelper.get_localized_string(LanguageHelper.TvShows): ( "https://www.tv4play.se/alla-program", None, False ), LanguageHelper.get_localized_string(LanguageHelper.Categories): ( "https://graphql.tv4play.se/graphql?query=query%7Btags%7D", None, False ), LanguageHelper.get_localized_string(LanguageHelper.CurrentlyPlayingEpisodes): ( self.__get_api_url("LiveVideos", "9b3d0d2f039089311cde2989760744844f7c4bb5033b0ce5643676ee60cb0901"), None, False ) } # No more extras # today = datetime.datetime.now() # days = [LanguageHelper.get_localized_string(LanguageHelper.Monday), # LanguageHelper.get_localized_string(LanguageHelper.Tuesday), # LanguageHelper.get_localized_string(LanguageHelper.Wednesday), # LanguageHelper.get_localized_string(LanguageHelper.Thursday), # LanguageHelper.get_localized_string(LanguageHelper.Friday), # LanguageHelper.get_localized_string(LanguageHelper.Saturday), # LanguageHelper.get_localized_string(LanguageHelper.Sunday)] # for i in range(0, 7, 1): # start_date = today - datetime.timedelta(i) # end_date = start_date + datetime.timedelta(1) # # day = days[start_date.weekday()] # if i == 0: # day = LanguageHelper.get_localized_string(LanguageHelper.Today) # elif i == 1: # day = LanguageHelper.get_localized_string(LanguageHelper.Yesterday) # # Logger.trace("Adding item for: %s - %s", start_date, end_date) # url = "https://api.tv4play.se/play/video_assets?exclude_node_nids=" \ # "&platform=tablet&is_live=false&product_groups=2&type=episode&per_page=100" # url = "%s&broadcast_from=%s&broadcast_to=%s&" % (url, start_date.strftime("%Y%m%d"), end_date.strftime("%Y%m%d")) # extras[day] = (url, start_date, False) # # extras[LanguageHelper.get_localized_string(LanguageHelper.CurrentlyPlayingEpisodes)] = ( # "https://api.tv4play.se/play/video_assets?exclude_node_nids=&platform=tablet&" # "is_live=true&product_groups=2&type=episode&per_page=100", None, False) # Actually add the extra items for name in extras: title = name url, date, is_live = extras[name] # type: str, datetime.datetime, bool item = MediaItem(title, url) item.dontGroup = True item.complete = True item.HttpHeaders = self.httpHeaders item.isLive = is_live if date is not None: item.set_date(date.year, date.month, date.day, 0, 0, 0, text=date.strftime("%Y-%m-%d")) items.append(item) Logger.debug("Pre-Processing finished") return data, items