def ProcessFolderList(self): """Wraps the channel.ProcessFolderList""" Logger.Info("Plugin::ProcessFolderList Doing ProcessFolderList") try: ok = True selectedItem = None if self.keywordPickle in self.params: selectedItem = Pickler.DePickleMediaItem(self.params[self.keywordPickle]) watcher = stopwatch.StopWatch("Plugin ProcessFolderList", Logger.Instance()) episodeItems = self.channelObject.ProcessFolderList(selectedItem) watcher.Lap("Class ProcessFolderList finished") if len(episodeItems) == 0: Logger.Warning("ProcessFolderList returned %s items", len(episodeItems)) ok = self.__ShowEmptyInformation(episodeItems) else: Logger.Debug("ProcessFolderList returned %s items", len(episodeItems)) xbmcItems = [] for episodeItem in episodeItems: if episodeItem.thumb == "": episodeItem.thumb = self.channelObject.noImage if episodeItem.fanart == "": episodeItem.fanart = self.channelObject.fanart if episodeItem.type == 'folder' or episodeItem.type == 'append' or episodeItem.type == "page": action = self.actionListFolder folder = True elif episodeItem.IsPlayable(): action = self.actionPlayVideo folder = False else: Logger.Critical("Plugin::ProcessFolderList: Cannot determine what to add") continue # Get the XBMC item item = episodeItem.GetXBMCItem() # Get the context menu items contextMenuItems = self.__GetContextMenuItems(self.channelObject, item=episodeItem) item.addContextMenuItems(contextMenuItems) # Get the action URL url = self.__CreateActionUrl(self.channelObject, action=action, item=episodeItem) # Add them to the list of XBMC items xbmcItems.append((url, 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, xbmcItems, len(xbmcItems)) watcher.Lap("items send to Kodi") if selectedItem is None: # mainlist item register channel. Statistics.RegisterChannelOpen(self.channelObject, Initializer.StartTime) watcher.Lap("Statistics send") watcher.Stop() self.__AddSortMethodToHandle(self.handle, episodeItems) # set the content xbmcplugin.setContent(handle=self.handle, content=self.contentType) xbmcplugin.endOfDirectory(self.handle, ok) except: Statistics.RegisterError(self.channelObject) XbmcWrapper.ShowNotification(LanguageHelper.GetLocalizedString(LanguageHelper.ErrorId), LanguageHelper.GetLocalizedString(LanguageHelper.ErrorList), XbmcWrapper.Error, 4000) Logger.Error("Plugin::Error Processing FolderList", exc_info=True) xbmcplugin.endOfDirectory(self.handle, False)
def CreateFolderItem(self, resultSet): """Creates a MediaItem of type 'folder' using the resultSet from the regex. Arguments: resultSet : tuple(strig) - the resultSet of the self.folderItemRegex Returns: A new MediaItem of type 'folder' This method creates a new MediaItem from the Regular Expression or Json results <resultSet>. The method should be implemented by derived classes and are specific to the channel. """ if len(resultSet) > 3 and resultSet[3] != "": Logger.Debug("Sub category folder found.") url = urlparse.urljoin( self.baseUrl, htmlentityhelper.HtmlEntityHelper.ConvertHTMLEntities( resultSet[3])) name = "\a.: %s :." % (resultSet[4], ) item = mediaitem.MediaItem(name, url) item.thumb = self.noImage item.complete = True item.type = "folder" return item url = urlparse.urljoin( self.baseUrl, htmlentityhelper.HtmlEntityHelper.ConvertHTMLEntities( resultSet[0])) name = htmlentityhelper.HtmlEntityHelper.ConvertHTMLEntities( resultSet[1]) helper = htmlhelper.HtmlHelper(resultSet[2]) description = helper.GetTagContent("div", {'class': 'description'}) item = mediaitem.MediaItem(name, "%s/RSS" % (url, )) item.thumb = self.noImage item.type = 'folder' item.description = description.strip() date = helper.GetTagContent("div", {'class': 'date'}) if date == "": date = helper.GetTagContent("span", {'class': 'lastPublishedDate'}) if not date == "": dateParts = Regexer.DoRegex("(\w+) (\d+)[^<]+, (\d+)", date) if len(dateParts) > 0: dateParts = dateParts[0] monthPart = dateParts[0].lower() dayPart = dateParts[1] yearPart = dateParts[2] try: month = datehelper.DateHelper.GetMonthFromName( monthPart, "en") item.SetDate(yearPart, month, dayPart) except: Logger.Error("Error matching month: %s", monthPart, exc_info=True) item.complete = True return item
def CreateVideoItemJson(self, resultSet): """Creates a MediaItem of type 'video' using the resultSet from the regex. Arguments: resultSet : tuple (string) - the resultSet of the self.videoItemRegex Returns: A new MediaItem of type 'video' or 'audio' (despite the method's name) This method creates a new MediaItem from the Regular Expression results <resultSet>. The method should be implemented by derived classes and are specific to the channel. If the item is completely processed an no further data needs to be fetched the self.complete property should be set to True. If not set to True, the self.UpdateVideoItem method is called if the item is focussed or selected for playback. """ Logger.Trace(resultSet) # get the title originalTitle = resultSet.get("original_title") localTitle = resultSet.get("local_title") # Logger.Trace("%s - %s", originalTitle, localTitle) if originalTitle == "": title = localTitle else: title = originalTitle # get the other meta data playLists = resultSet.get("local_playlists", []) videoMgid = None for playList in playLists: language = playList["language_code"] if language == self.language: Logger.Trace("Found '%s' playlist, using this one.", language) videoMgid = playList["id"] break elif language == "en": Logger.Trace("Found '%s' instead of '%s' playlist", language, self.language) videoMgid = playList["id"] if videoMgid is None: Logger.Error("No video MGID found for: %s", title) return None url = "http://api.mtvnn.com/v2/mrss.xml?uri=mgid:sensei:video:mtvnn.com:local_playlist-%s" % ( videoMgid, ) thumb = resultSet.get("riptide_image_id") thumb = "http://images.mtvnn.com/%s/original" % (thumb, ) description = resultSet.get("local_long_description") date = resultSet.get("published_from") date = date[0:10].split("-") item = mediaitem.MediaItem(title, url) item.thumb = thumb item.description = description item.icon = self.icon item.type = 'video' item.SetDate(date[0], date[1], date[2]) item.complete = False return item
def Send(templateFilePath: str, commits: list) -> ErrorCode: """ Send an HTML email of the given commits in the style of the given template file. Use the email settings (subject, to, from) from the config.json. Args: templateFilePath (str): The path to the email template file. commits (list): The list of commits to list in the email. Returns: An ErrorCode object telling what the outcome of calling the function was. """ COMMIT_LIST_ITEM_FORMAT = \ """ <li> {title} {author} {date} {message} </li> """ config = Config.GetConfig() email = EmailMessage() email['Subject'] = config.get('Email').get('Subject') email['To'] = config.get('Email').get('To') email['From'] = config.get('Email').get('From') version = "" author = "" if (len(commits)): author = commits[0].author version = commits[0].tag textTemplate = "" htmlPart = "" try: with open(templateFilePath, 'r') as templateFile: textTemplate = templateFile.read() except IOError as err: Logger.Error(LOG_TAG, err) return ErrorCode.FILE_ERROR if (len(textTemplate) == 0): Logger.Error(LOG_TAG, "Template file empty") return ErrorCode.FILE_ERROR htmlPart = textTemplate.format( title=config.get('Email').get('Subject'), version=version, author=author, changeLog='\n'.join( list( map(lambda x: HTMLEmail.COMMIT_LIST_ITEM_FORMAT.format( title=x.title, author=x.author, date=Date.ConvertDateToString(x.date), message=x.message))))) email.set_content(htmlPart) email.add_alternative(htmlPart, subtype='html') smtp = smtplib.SMTP(config.get('Email').get('SMTP').get('Server')) smtp.send(email) smtp.quit() return ErrorCode.OK
def PrintSettingValues(): """Prints the settings""" pattern = "%s\n%s: %s" value = "%s: %s" % ("ClientId", AddonSettings.GetClientId()) value = pattern % (value, "MaxStreamBitrate", AddonSettings.GetMaxStreamBitrate()) value = pattern % (value, "SortingAlgorithm", AddonSettings.GetSortAlgorithm()) value = pattern % (value, "UseSubtitle", AddonSettings.UseSubtitle()) value = pattern % (value, "CacheHttpResponses", AddonSettings.CacheHttpResponses()) value = pattern % (value, "Folder Prefx", "'%s'" % AddonSettings.GetFolderPrefix()) value = pattern % (value, "Empty List Behaviour", AddonSettings.GetEmptyListBehaviour()) value = pattern % (value, "ListLimit", AddonSettings.GetListLimit()) value = pattern % (value, "Loglevel", AddonSettings.GetLogLevel()) value = pattern % (value, "Geo Location", AddonSettings.HideGeoLockedItemsForLocation( None, valueOnly=True)) value = pattern % (value, "Filter Folders", AddonSettings.HideRestrictedFolders()) value = pattern % (value, "DRM Warning", AddonSettings.ShowDrmWarning()) value = pattern % (value, "Hide DRM Items", AddonSettings.HideDrmItems()) value = pattern % (value, "Hide Premium Items", AddonSettings.HidePremiumItems()) value = pattern % (value, "Show Dutch", AddonSettings.ShowChannelWithLanguage("nl")) value = pattern % (value, "Show Swedish", AddonSettings.ShowChannelWithLanguage("se")) value = pattern % (value, "Show Lithuanian", AddonSettings.ShowChannelWithLanguage("lt")) value = pattern % (value, "Show Latvian", AddonSettings.ShowChannelWithLanguage("lv")) # value = pattern % (value, "Show French Canadian", AddonSettings.ShowChannelWithLanguage("ca-fr")) # value = pattern % (value, "Show English Canadian", AddonSettings.ShowChannelWithLanguage("ca-en")) value = pattern % (value, "Show British", AddonSettings.ShowChannelWithLanguage("en-gb")) value = pattern % (value, "Show German", AddonSettings.ShowChannelWithLanguage("de")) # noinspection PyTypeChecker value = pattern % (value, "Show Other languages", AddonSettings.ShowChannelWithLanguage(None)) value = pattern % (value, "UZG Cache Path", AddonSettings.GetUzgCachePath()) value = pattern % (value, "UZG Cache Time", AddonSettings.GetUzgCacheDuration()) try: proxies = ["NL", "UK", "SE", "Other"] for proxy in proxies: value = pattern % (value, "%s Proxy" % (proxy, ), AddonSettings.__GetSetting( "%s_proxy_server" % (proxy.lower(), )) or "Not Set") value = pattern % (value, "%s Proxy Port" % (proxy, ), AddonSettings.__GetSetting( "%s_proxy_port" % (proxy.lower(), )) or 0) except: Logger.Error("Error", exc_info=True) return value
def UpdateVideoItem(self, item): """ Accepts an item. It returns an updated item. Usually retrieves the MediaURL and the Thumb! It should return a completed item. """ Logger.Debug('Starting UpdateVideoItem for %s (%s)', item.name, self.channelName) # get additional info data = UriHandler.Open(item.url, proxy=self.proxy) guid = Regexer.DoRegex( '<meta property="og:video" content="http://player.extreme.com/FCPlayer.swf\?id=([^&]+)&[^"]+" />', data) #<param name="flashvars" value="id=dj0xMDEzNzQyJmM9MTAwMDAwNA&tags=source%253Dfreecaster&autoplay=1" /> # http://freecaster.tv/player/smil/dj0xMDEzNzQyJmM9MTAwMDAwNA -> playlist with bitrate # http://freecaster.tv/player/smil/dj0xMDEzNzQyJmM9MTAwMDAwNA -> info (not needed, get description from main page. if len(guid) > 0: url = '%s/player/smil/%s' % ( self.baseUrl, guid[0], ) data = UriHandler.Open(url) smiller = Smil(data) baseUrl = smiller.GetBaseUrl() urls = smiller.GetVideosAndBitrates() part = item.CreateNewEmptyMediaPart() for url in urls: if "youtube" in url[0]: for s, b in YouTube.GetStreamsFromYouTube( url[0], self.proxy): item.complete = True part.AppendMediaStream(s, b) else: part.AppendMediaStream("%s%s" % (baseUrl, url[0]), bitrate=int(int(url[1]) / 1000)) item.complete = True Logger.Trace("UpdateVideoItem complete: %s", item) return item # Try the brightcove brightCoveRegex = '<object id="myExperience[\w\W]+?videoPlayer" value="(\d+)"[\w\W]{0,1000}?playerKey" value="([^"]+)' brightCoveData = Regexer.DoRegex(brightCoveRegex, data) Logger.Trace(brightCoveData) if len(brightCoveData) > 0: seed = "c5f9ae8729f7054d43187989ef3421531ee8678d" objectData = brightCoveData[0] # from proxyinfo import ProxyInfo playerKey = str(objectData[1]) videoId = int(objectData[0]) part = item.CreateNewEmptyMediaPart() # But we need the IOS streams! amfHelper = BrightCove(Logger.Instance(), playerKey, videoId, str(item.url), seed, proxy=self.proxy) for stream, bitrate in amfHelper.GetStreamInfo( renditions="IOSRenditions"): part.AppendMediaStream(stream, bitrate) Logger.Error("Cannot find GUID in url: %s", item.url) return item
def __RetreiveData(self, destHandle, uri, timeOutValue, progressCallback=None, proxy=None, maxBytes=0, params="", referer=None, additionalHeaders=None, noCache=False, blockMultiplier=1): """Open an URL Async using a thread Arguments: uri : string - the URI to download Keyword Arguments: progressCallback : [opt] boolean - should a progress bar be shown proxy : [opt] string - The address and port (proxy.address.ext:port) of a proxy server that should be used. bytes : [opt] integer - the number of bytes to get. params : [opt] string - data to send with the request (open(uri, params)) headers : [opt] dict - a dictionary of additional headers Returns: The data that was retrieved from the URI. """ # init parameters canceled = False timeOut = False error = False srcHandle = None blocksRead = 0 fileSize = 0 charSet = None blockSize = self.blockSize * blockMultiplier try: if uri == "": return error, canceled, "" if uri.startswith("file:"): index = uri.rfind("?") #index = string.rfind(uri, "?") if index > 0: uri = uri[0:index] Logger.Info( "Opening requested uri: %s (callback=%s, timeout=%s)", uri, progressCallback is not None, timeOutValue) self.__DoCallback(progressCallback, 0, blockSize, 0, False) # set the start time in seconds startTime = time.time() # get an opener and handle opener = self.__GetOpener(uri, proxy, disableCaching=noCache, referer=referer, additionalHeaders=additionalHeaders) if params == '': srcHandle = opener.open(uri) else: srcHandle = opener.open(uri, params) # get some metadata Logger.Debug("Determining number of bytes to fetch") data = srcHandle.info() if data.get('Content-length'): fileSize = int(data.get('Content-length')) Logger.Debug('ByteSize is known (fileSize=' + str(fileSize) + ')') else: fileSize = -1 Logger.Debug('ByteSize is unknown') # check for encoding charSet = None try: contentType = data.get('Content-Type') if contentType: Logger.Trace("Found Content-Type header: %s", contentType) charSetNeedle = 'charset=' charSetIndex = contentType.rfind(charSetNeedle) if charSetIndex > 0: charSetEndIndex = contentType.find( ";", charSetIndex) if charSetEndIndex > 0: charSet = contentType[charSetIndex + len(charSetNeedle ):charSetEndIndex] else: charSet = contentType[charSetIndex + len(charSetNeedle):] Logger.Trace("Found Charset HTML Header: %s", charSet) except: charSet = None blocksRead = 0 while True: block = srcHandle.read(blockSize) if block == "": break destHandle.write(block) blocksRead += 1 canceled = self.__DoCallback(progressCallback, blocksRead, blockSize, fileSize, False) if canceled: break if time.time() > startTime + timeOutValue: timeOut = True break if 0 < maxBytes < blocksRead * blockSize: Logger.Info( 'Stopping download because Bytes > maxBytes') break srcHandle.close() except (IncompleteRead, ValueError): # Python 2.6 throws a IncompleteRead on Chuncked data # Python 2.4 throws a ValueError on Chuncked data Logger.Error("IncompleteRead error opening url %s", uri) try: if srcHandle: srcHandle.close() except UnboundLocalError: pass except: Logger.Critical("Error Opening url %s", uri, exc_info=True) error = True try: if srcHandle: srcHandle.close() except UnboundLocalError: pass # we are finished now self.__DoCallback(progressCallback, blocksRead, blockSize, fileSize, True) if timeOut: Logger.Critical( "The URL lookup did not respond within the TimeOut (%s s)", timeOutValue) elif canceled: Logger.Warning("Opening of %s was canceled", uri) elif not error: Logger.Info("Url %s was opened successfully", uri) if self.cookieJarFile: # noinspection PyUnresolvedReferences self.cookieJar.save() return error, canceled, charSet
def __UpdateAddOnSettingsWithChannelSettings(contents, channels): """ Adds the channel specific settings @param contents: The current settings @param channels: The available channels @return: updated contents and the offset in visibility This method first aggregates the settings and then adds them. """ if "<!-- begin of channel settings -->" not in contents: Logger.Error( "No '<!-- begin of channel settings -->' found in settings.xml. Stopping updating." ) return settings = dict() # There are 2 settings between the selector list and the channel settings in the settings_template.xml settingOffsetForVisibility = 2 # Let's make sure they are sorted by channel module. So we first go through them all and then create # the XML. for channel in channels: if channel.moduleName not in settings: settings[channel.moduleName] = [] # add channel visibility settingXml = '<setting id="channel_%s_visible" type="bool" label="30042" ' \ 'default="true" visible="eq(-%%s,%s)" />' % \ (channel.guid, channel.safeName) Logger.Trace(settingXml) settings[channel.moduleName].append(settingXml) if not channel.settings: continue # Sort the settings so they are really in the correct order, because this is not guaranteed by the # json parser channel.settings.sort(lambda a, b: cmp(a["order"], b["order"])) for channelSettings in channel.settings: settingId = channelSettings["id"] settingValue = channelSettings["value"] Logger.Debug("Adding setting: '%s' with value '%s'", settingId, settingValue) if settingValue.startswith("id="): settingXml = "<setting %s visible=\"eq(-%%s,%s)\" />" % \ (settingValue, channel.safeName) else: settingXml = '<setting id="channel_%s_%s" %s visible=\"eq(-%%s,%s)\" />' % \ (channel.guid, settingId, settingValue, channel.safeName) settings[channel.moduleName].append(settingXml) xmlContent = '\n <!-- begin of channel settings -->\n' # Sort them to make the result more consistent settingKeys = settings.keys() settingKeys.sort() for module in settingKeys: xmlContent = '%s <!-- %s.py -->\n' % (xmlContent, module) for setting in settings[module]: settingOffsetForVisibility += 1 xmlContent = "%s %s\n" % (xmlContent, setting % settingOffsetForVisibility) begin = contents[:contents.find('<!-- begin of channel settings -->' )].strip() end = contents[contents.find('<!-- end of channel settings -->'):] Logger.Trace("Generated channel settings:\n%s", xmlContent) contents = "%s\n%s\n %s" % (begin, xmlContent.rstrip(), end) return contents, settingOffsetForVisibility
def SetDate(self, year, month, day, hour=None, minutes=None, seconds=None, onlyIfNewer=False, text=None): """Sets the datetime of the MediaItem Arguments: year : integer - the year of the datetime month : integer - the month of the datetime day : integer - the day of the datetime Keyword Arguments: hour : [opt] integer - the hour of the datetime minutes : [opt] integer - the minutes of the datetime seconds : [opt] integer - the seconds of the datetime onlyIfNewer: [opt] integer - update only if the new date is more recent then the currently set one text : [opt] string - if set it will overwrite the text in the date label the datetime is also set. 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 <onlyIfNewer> 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. @return: the datetime that was set. """ # dateFormat = xbmc.getRegion('dateshort') # correct a small bug in XBMC # dateFormat = dateFormat[1:].replace("D-M-", "%D-%M") # dateFormatLong = xbmc.getRegion('datelong') # timeFormat = xbmc.getRegion('time') # dateTimeFormat = "%s %s" % (dateFormat, timeFormat) try: dateFormat = "%Y-%m-%d" # "%x" dateTimeFormat = dateFormat + " %H:%M" if hour is None and minutes is None and seconds is None: timeStamp = datetime.datetime(int(year), int(month), int(day)) date = timeStamp.strftime(dateFormat) else: timeStamp = datetime.datetime(int(year), int(month), int(day), int(hour), int(minutes), int(seconds)) date = timeStamp.strftime(dateTimeFormat) if onlyIfNewer and self.__timestamp > timeStamp: return self.__timestamp = timeStamp 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.datetime.min self.__date = "" return self.__timestamp
def __init__(self, title, url, type="folder"): """Creates a new MediaItem Arguments: title : string - the title of the item, used for appearance in lists. url : string - url that used for further information retrieval. Keyword Arguments: type : [opt] string - type of MediaItem (folder, video, audio). Defaults to 'folder'. parent : [opt] MediaItem - the parent of the current item. None is the default. 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 XBMC items in the different methods. """ name = title.strip() self.name = name self.url = url 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.datetime.min # : value for sorting, this one is set to minimum so if non is set, it's shown at the bottom 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.downloaded = False self.downloadable = False self.items = [] self.HttpHeaders = dict() # : http headers for the item data retrieval self.rating = None # 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.EncodeMD5(title), EncodingHelper.EncodeMD5(url or "")) # self.guid = ("%s-%s" % (encodinghelper.EncodingHelper.EncodeMD5(title), url)).replace(" ", "") except: Logger.Error( "Error setting GUID for title:'%s' and url:'%s'. Falling back to UUID", title, url, exc_info=True) self.guid = self.__GetUUID() self.guidValue = int("0x%s" % (self.guid, ), 0) self.channels = [] # only needed for Kanalenkiezer
def __ImportChannels(self): # , className = None): """Import the available channels This method will: - iterate through the Addons folder and find all the folders name <basename>.channel.<channelname>. - then adds all the subfolders into a list (with paths). - then all paths are added to the system path, so they can be imported. - then read all the chn_<name>.xml metadata files and add the ChannelInfo objects to the self.__channelsToImport - then the channels in the self.__channelsToImport list are instantiated into the self.channels list. """ Logger.Debug("Importing available channels") # import each channelPath. On import, the channelPath will call the RegisterChannel Method try: # clear a possible previous import self.__enabledChannels = [] self.__allChannels = [] self.__validChannels = [] self.__channelVersions = [] # first find all folders with channels that we might need to import channelImport = [] importTimer = StopWatch("ChannelImporter :: importing channels", Logger.Instance()) addonPath = self.__GetAddonPath() channelPathStart = "%s.channel" % (Config.addonDir,) for directory in os.listdir(addonPath): if channelPathStart in directory and "BUILD" not in directory: path = os.path.join(addonPath, directory) channelVersion = self.__ParseChannelVersionInfo(path) if channelVersion: self.__channelVersions.append(channelVersion) else: # no info was returned, so we will not include the channel continue # get all nested channels subDirs = os.listdir(path) channelImport.extend( [os.path.abspath(os.path.join(path, weapon)) for weapon in subDirs]) channelImport.sort() importTimer.Lap("Directories scanned for .channel") # we need to make sure we don't load multiple channel classes and track if we found updates channelsUpdated = False loadedChannels = [] channelsToImport = [] # now import the channels for channelPath in channelImport: if not os.path.isdir(channelPath): continue # determine channelname channelName = os.path.split(channelPath)[-1] if channelName == self.__updateChannelPath: Logger.Trace("Update path found and skipping: %s", channelName) continue # if loadedChannels.count(channelName) > 0: if channelName in loadedChannels: Logger.Warning( "Not loading: chn_%s.xml in %s because there is already a path with " "name '%s' that name loaded", channelName, channelPath, channelName) continue if channelName.startswith("."): continue # now we can continue loadedChannels.append(channelName) fileName = os.path.join(channelPath, "chn_" + channelName + ".json") Logger.Trace("Loading info for chn_%s @ %s", channelName, fileName) if os.path.isfile(fileName): try: ci = ChannelInfo.FromJson(fileName) if len(ci) <= 0: Logger.Warning("No channels found in '%s'", fileName) continue # Add them to the list to import channelsToImport += ci if self.__IsChannelSetUpdated(ci[0]): if not channelsUpdated: # this was the first update found (otherwise channelsUpdated was True) show a message: title = LanguageHelper.GetLocalizedString( LanguageHelper.InitChannelTitle) text = LanguageHelper.GetLocalizedString( LanguageHelper.InitChannelText) XbmcWrapper.ShowNotification(title, text, displayTime=15000) # set the updates found bit channelsUpdated |= True # Initialise the channelset. self.__InitialiseChannelSet(ci[0]) # And perform all first actions for the included channels in the set for channelInfo in ci: self.__FirstTimeChannelActions(channelInfo) except: Logger.Error("Error import chn_%s.json", channelName, exc_info=True) importTimer.Lap() # What platform are we platform = envcontroller.EnvController.GetPlatform() # instantiate the registered channels for channelInfo in channelsToImport: # noinspection PyUnusedLocal isValid = self.__ValidateChannelInfo(channelInfo, platform) # sort the channels self.__enabledChannels.sort() if channelsUpdated: Logger.Info("New or updated channels found. Updating add-on configuration for all " "channels and user agent") AddonSettings.UpdateAddOnSettingsWithChannels(self.__validChannels, Config) AddonSettings.UpdateUserAgent() else: Logger.Debug("No channel changes found. Skipping add-on configuration for channels") # Should we update the channel index? if channelsUpdated or not os.path.isfile(self.__CHANNEL_INDEX): self.__CreateChannelIndex() Logger.Info("Imported %s channels from which %s are enabled", len(self.__allChannels), len(self.__enabledChannels)) importTimer.Stop() except: Logger.Critical("Error loading channel modules", exc_info=True)
def GetSingleChannel(self, className, channelCode): """Imports a single channel Arguments: className : string - class name of the channel to import. Returns: The channels in the requested class. So that could be more, but they can be distinguished using the channelcode. Returns an empty list if no channels were found. """ if not className: raise ValueError("className should be specified.") Logger.Info("Loading channels for class '%s' and channelCode '%s'", className, channelCode) self.__enabledChannels = [] self.__allChannels = [] self.__validChannels = [] self.__channelVersions = [] # noinspection PyUnusedLocal classPath = None channelPath = None classBaseName = className[4:] if os.path.isfile(self.__CHANNEL_INDEX): Logger.Debug("Using ChannelIndex for channel lookup: %s", self.__CHANNEL_INDEX) fd = None try: fd = open(self.__CHANNEL_INDEX) data = fd.read() finally: if fd is not None and not fd.closed: fd.close() channelIndex = JsonHelper(data) classPath = channelIndex.GetValue(className, channelCode or "null") if classPath is not None: if not os.path.isdir(classPath): Logger.Warning("Missing channel class path '%s' found. Rebuilding the ChannelIndex.", classPath) # remove the old one os.remove(self.__CHANNEL_INDEX) # return self.GetSingleChannel(className, channelCode) return self.__ImportChannel(className, channelCode) channelPath = os.path.join(classPath, "..") else: Logger.Warning("Missing ChannelIndex. Rebuilding the ChannelIndex.") return self.__ImportChannel(className, channelCode) # Logger.Warning("Falling back to classic find pattern") # # # walk the channel dirs to find the one that has the channel # addonPath = self.__GetAddonPath() # channelPathStart = "%s.channel" % (Config.addonDir,) # # # list all add-ons # for directory in os.listdir(addonPath): # # find the xot ones # if channelPathStart in directory and "BUILD" not in directory: # channelPath = os.path.join(addonPath, directory) # # # list the subfolders for the requested folder to find the one we need # if classBaseName not in os.listdir(channelPath): # continue # # # we perhaps found it. # classPath = os.path.join(channelPath, classBaseName) if classPath is None: Logger.Error("No Channel found for class '%s' and channelCode '%s'", className, channelCode) return None Logger.Debug("Found possible channel folder in %s", classPath) # check the addon.xml with self.__ParseChannelVersionInfo(path) channelVersion = self.__ParseChannelVersionInfo(channelPath) if channelVersion: self.__channelVersions.append(channelVersion) else: # no info was returned, so we will not include the channel Logger.Error("Match in %s has incorrect version", classPath) return None # create ChannelInfo objects from the xml file and get the correct ChannelInfo object. It coulde that none # is found and we might need to continue (in case there were duplicate channel names fileName = os.path.join(classPath, "chn_" + classBaseName + ".json") Logger.Debug("Loading info for chn_%s @ %s", classBaseName, fileName) if not os.path.isfile(fileName): Logger.Error("Could not load %s", fileName) return None cis = ChannelInfo.FromJson(fileName) ci = filter(lambda c: c.moduleName == className and (c.channelCode == channelCode or c.channelCode is channelCode), cis) if not ci or len(ci) > 1: Logger.Error("Could not load channel with className=%s and channelCode=%s from %s", className, channelCode, fileName) return None ci = ci[0] if self.__IsChannelSetUpdated(ci): # apparently a new channel was found, so we need to do it all Logger.Info("Found a new channel, we need to reload all channels") return self.__ImportChannel(className, channelCode) # What platform are we platform = envcontroller.EnvController.GetPlatform() # check if it is enabled or not if self.__ValidateChannelInfo(ci, platform): return ci.GetChannel() else: Logger.Error("Invalid Channel found for class '%s' and channelCode '%s'", className, channelCode) return None
def GetSingleChannel_old(self, className, channelCode): """Imports a single channel Arguments: className : string - class name of the channel to import. Returns: The channels in the requested class. So that could be more, but they can be distinguished using the channelcode. Returns an empty list if no channels were found. """ if not className: raise ValueError("className should be specified.") Logger.Info("Loading channels for class '%s' and channelCode '%s'", className, channelCode) self.__enabledChannels = [] self.__allChannels = [] self.__validChannels = [] self.__channelVersions = [] channel = None # walk the channel dirs to find the one that has the channel addonPath = self.__GetAddonPath() channelPathStart = "%s.channel" % (Config.addonDir, ) folderToFind = className[4:] # list all add-ons for directory in os.listdir(addonPath): # find the xot ones if channelPathStart in directory and "BUILD" not in directory: channelPath = os.path.join(addonPath, directory) # list the subfolders for the requested folder to find the one we need if folderToFind not in os.listdir(channelPath): continue # we perhaps found it. classPath = os.path.join(channelPath, folderToFind) Logger.Debug("Found possible channel folder in %s", classPath) # check the addon.xml with self.__ParseChannelVersionInfo(path) channelVersion = self.__ParseChannelVersionInfo(channelPath) if channelVersion: self.__channelVersions.append(channelVersion) else: # no info was returned, so we will not include the channel Logger.Warning("Match in %s has incorrect version", classPath) continue # create ChannelInfo objects from the xml file and get the correct ChannelInfo object. It coulde that none # is found and we might need to continue (in case there were duplicate channel names fileName = os.path.join(classPath, "chn_" + folderToFind + ".json") Logger.Debug("Loading info for chn_%s @ %s", folderToFind, fileName) if not os.path.isfile(fileName): Logger.Warning("Could not load %s", fileName) continue cis = ChannelInfo.FromJson(fileName) ci = filter(lambda c: c.moduleName == className and (c.channelCode == channelCode or c.channelCode is channelCode), cis) if not ci or len(ci) > 1: Logger.Warning("Could not load channel with className=%s and channelCode=%s from %s", className, channelCode, fileName) continue ci = ci[0] if self.__IsChannelSetUpdated(ci): # apparently a new channel was found, so we need to do it all Logger.Info("Found a new channel, we need to reload all channels") return self.__ImportChannel(className, channelCode) # What platform are we platform = envcontroller.EnvController.GetPlatform() # check if it is enabled or not if self.__ValidateChannelInfo(ci, platform): return ci.GetChannel() else: continue Logger.Error("No Channel found for class '%s' and channelCode '%s'", className, channelCode) return channel
def __ConvertDCSubtitleToSrt(dcSubtitle): """Converts DC Subtitle format into SRT format: Arguments: dcSubtitle : string - DC Subtitle subtitle format Returns: SRT formatted subtitle: Example: <Subtitle SpotNumber="1" TimeIn="00:00:01:220" TimeOut="00:00:04:001" FadeUpTime="20" FadeDownTime="20"> <Text Direction="horizontal" HAlign="center" HPosition="0.0" VAlign="bottom" VPosition="6.0">Line 1</Text> </Subtitle> <Subtitle SpotNumber="2" TimeIn="00:02:07:180" TimeOut="00:02:10:040" FadeUpTime="20" FadeDownTime="20"> <Text Direction="horizontal" HAlign="center" HPosition="0.0" VAlign="bottom" VPosition="6.0">Line 1</Text> </Subtitle> <Subtitle SpotNumber="3" TimeIn="00:02:15:190" TimeOut="00:02:17:190" FadeUpTime="20" FadeDownTime="20"> <Text Direction="horizontal" HAlign="center" HPosition="0.0" VAlign="bottom" VPosition="14.0">Line 1</Text> <Text Direction="horizontal" HAlign="center" HPosition="0.0" VAlign="bottom" VPosition="6.0">Line 2</Text> </Subtitle> <Subtitle SpotNumber="4" TimeIn="00:03:23:140" TimeOut="00:03:30:120" FadeUpTime="20" FadeDownTime="20"> <Text Direction="horizontal" HAlign="center" HPosition="0.0" VAlign="bottom" VPosition="14.0">Line 1</Text> <Text Direction="horizontal" HAlign="center" HPosition="0.0" VAlign="bottom" VPosition="14.0">Line 2</Text> <Text Direction="horizontal" HAlign="center" HPosition="0.0" VAlign="bottom" VPosition="14.0">Line 3</Text> </Subtitle> Returns 1 00:00:20,000 --> 00:00:24,400 text The format of the timecode is Hours:Minutes:Seconds:Ticks where a "Tick" is a value of between 0 and 249 and lasts 4 milliseconds. """ # parseRegex = '<subtitle[^>]+spotnumber="(\d+)" timein="(\d+:\d+:\d+):(\d+)" timeout="(\d+:\d+:\d+):(\d+)"[^>]+>\W+<text[^>]+>([^<]+)</text>\W+(?:<text[^>]+>([^<]+)</text>)*\W+</subtitle>' parseRegex = '<subtitle[^>]+spotnumber="(\d+)" timein="(\d+:\d+:\d+):(\d+)" timeout="(\d+:\d+:\d+):(\d+)"[^>]+>|<text[^>]+>([^<]+)</text>' parseRegex = parseRegex.replace('"', '["\']') subs = Regexer.DoRegex(parseRegex, dcSubtitle) srt = "" i = 1 text = "" start = "" end = "" for sub in subs: #Logger.Trace(sub) try: if sub[0]: # new start of a sub if text and start and end: # if we have a complete old one, save it text = htmlentityhelper.HtmlEntityHelper.ConvertHTMLEntities( text) srt = "%s\n%s\n%s --> %s\n%s\n" % (srt, i, start, end, text.strip()) i += 1 start = "%s,%03d" % (sub[1], int(sub[2]) * 4) end = "%s,%03d" % (sub[3], int(sub[4]) * 4) text = "" else: text = "%s\n%s" % (text, sub[5].replace("<br />", "\n")) except: Logger.Error("Error parsing subtitle: %s", sub, exc_info=True) return srt
def __RebuildIndex(self): # type: () -> dict """ Rebuilds the channel index that contains all channels and performs all necessary steps: 1. Find all channel add-on paths and determine the version of the channel add-on 2. For all channel sets in the add-on: a. See if it is a new channel set (pyo and pyc check) b. If so, initialise the channel set and then perform the first time actions on the included channels. c. Add all channels within the channel set to the channelIndex @return: the new channel index dictionary object. Remark: this method only generates the index of the channels, it does not import at all! """ if self.__reindexed: Logger.Error("Channel index was already re-indexed this run. Not doing it again.") return self.__channelIndex Logger.Info("Rebuilding the channel index.") index = { self.__CHANNEL_INDEX_ADD_ONS_KEY: [], self.__CHANNEL_INDEX_CHANNEL_KEY: {} } # iterate all Retrospect Video Add-ons addonPath = self.__GetAddonPath() channelPathStart = "%s.channel" % (Config.addonDir,) addOns = filter(lambda x: channelPathStart in x and "BUILD" not in x, os.listdir(addonPath)) for addOnDir in addOns: index[self.__CHANNEL_INDEX_ADD_ONS_KEY].append(addOnDir) channelAddOnPath = os.path.join(addonPath, addOnDir) channelAddOnId, channelAddOnVersion = self.__ValidateAddOnVersion(channelAddOnPath) if channelAddOnId is None: continue channelSets = os.listdir(channelAddOnPath) for channelSet in channelSets: if not os.path.isdir(os.path.join(channelAddOnPath, channelSet)): continue channelSetId = "chn_%s" % (channelSet,) Logger.Debug("Found channel set '%s'", channelSetId) index[self.__CHANNEL_INDEX_CHANNEL_KEY][channelSetId] = { self.__CHANNEL_INDEX_CHANNEL_VERSION_KEY: str(channelAddOnVersion), self.__CHANNEL_INDEX_CHANNEL_INFO_KEY: os.path.join(channelAddOnPath, channelSet, "%s.json" % (channelSetId,)) } f = None try: f = open(self.__CHANNEL_INDEX, 'w+') f.write(JsonHelper.Dump(index)) finally: if f is not None: f.close() # now we marked that we already re-indexed. self.__reindexed = True self.__channelIndex = index Logger.Info("Rebuilding channel index completed with %d channelSets and %d add-ons: %s.", len(index[self.__CHANNEL_INDEX_CHANNEL_KEY]), len(index[self.__CHANNEL_INDEX_ADD_ONS_KEY]), index) envcontroller.EnvController.UpdateLocalAddons() return index
def DownloadSubtitle(url, fileName="", format='sami', proxy=None): """Downloads a SAMI and stores the SRT in the cache folder Arguments: url : string - URL location of the SAMI file Keyword Arguments: fileName : string - Filename to use to store the subtitle in SRT format. if not specified, an MD5 hash of the URL with .xml extension will be used format : string - defines the source format. Defaults to Sami. Returns: The full patch of the cached SRT file. """ if fileName == "": Logger.Debug( "No filename present, generating filename using MD5 hash of url." ) fileName = "%s.srt" % ( encodinghelper.EncodingHelper.EncodeMD5(url), ) elif not fileName.endswith(".srt"): Logger.Debug("No SRT extension present, appending it.") fileName = "%s.srt" % (fileName, ) srt = "" try: localCompletePath = os.path.join(Config.cacheDir, fileName) # no need to download it again! if os.path.exists(localCompletePath): return localCompletePath Logger.Trace("Opening Subtitle URL") raw = UriHandler.Open(url, proxy=proxy) if raw == "": Logger.Warning( "Empty Subtitle path found. Not setting subtitles.") return "" # try to decode it try: raw = raw.decode() except: Logger.Warning( "Converting input to UTF-8 using 'unicode_escape'") raw = raw.decode('unicode_escape') if format.lower() == 'sami': srt = SubtitleHelper.__ConvertSamiToSrt(raw) elif format.lower() == 'srt': srt = raw elif format.lower() == 'ttml': srt = SubtitleHelper.__ConvertTtmlToSrt(raw) elif format.lower() == 'dcsubtitle': srt = SubtitleHelper.__ConvertDCSubtitleToSrt(raw) elif format.lower() == 'json': srt = SubtitleHelper.__ConvertJsonSubtitleToSrt(raw) else: error = "Uknown subtitle format: %s" % (format, ) raise NotImplementedError(error) f = open(localCompletePath, 'w') f.write(srt) f.close() Logger.Info("Saved SRT as %s", localCompletePath) return localCompletePath except: Logger.Error("Error handling Subtitle file: [%s]", srt, exc_info=True) return ""
def InitSimpleExpressApp(self, args): if self.conf['app-type'] == 'virtualized' or self.conf['app-type'] == 'simple-express-app': return Logger.Error("This is already an {0} based repo, can't add this feature".format(self.conf['app-type'])) return EF.Extend('express', args)
def __UpdateAddOnSettingsWithLanguages(contents, channels): """ Adds the channel showing/hiding to the settings.xml @param contents: The current settings @param channels: The available channels @return: updated contents and the offset in visibility """ if "<!-- start of channel selection -->" not in contents: Logger.Error( "No '<!-- start of channel selection -->' found in settings.xml. Stopping updating." ) return # First we create a new bit of settings file. channelXml = ' <!-- start of channel selection -->\n' # the distinct list of languages from the channels languages = map(lambda c: c.language, channels) languages = list(set(languages)) languages.sort() Logger.Debug("Found languages: %s", languages) # get the labels and setting identifiers for those languages languageLookup = dict() for language in languages: languageLookup[ language] = AddonSettings.__GetLanguageSettingsIdAndLabel( language) languageLookupSortedKeys = languageLookup.keys() languageLookupSortedKeys.sort() # create a list of labels languageLabels = map(lambda l: str(languageLookup[l][1]), languageLookupSortedKeys) channelXml = '%s <setting type="lsep" label="30060" />\n' % ( channelXml, ) channelXml = '%s <setting id="channel_selected" label="30061" type="labelenum" lvalues="30025|%s" />\n' % ( channelXml, "|".join(languageLabels), ) # we need to keep track of the number of lines, because we have # relative visible and enable settings. currentLine = 0 # the current line we are writing channelXml = '%s <setting type="sep" />\n' % (channelXml, ) currentLine += 1 # first add the overall language settings for language in languageLookupSortedKeys: currentLine += 1 languageIndex = languageLookupSortedKeys.index( language) + 1 # correct of the None label channelXml = '%s <setting id="%s" type="bool" label="30042" default="true" visible="eq(-%s,%s)" /><!-- %s -->\n' % ( channelXml, languageLookup[language][0], currentLine, languageIndex, languageLookup[language][1]) # then the channels for channel in channels: currentLine += 1 name = channel.channelName languageIndex = languageLookupSortedKeys.index( channel.language) + 1 # correct of the None label channelXml = '%s <setting id="%s" type="bool" label="- %s" default="true" visible="eq(-%s,%s)" enable="eq(-%s,True)" />\n' % ( channelXml, AddonSettings.__CHANNEL_SETTINGS_PATTERN % (channel.guid, ), name, currentLine, languageIndex, currentLine - languageIndex - 1) begin = contents[:contents.find('<!-- start of channel selection -->' )].strip() end = contents[contents.find('<!-- end of channel selection -->' ):].strip() contents = "%s\n \n%s %s" % (begin, channelXml, end) return contents
def AddDocker(self, args): if self.conf['app-type'] == 'non-virtualized': return Logger.Error("This is already an {0} based repo, can't add this feature".format(self.conf['app-type'])) return EF.Extend('docker', args)
def GetStreamsFromYouTube(url, proxy=None): """ Parsers standard YouTube videos and returns a list of tuples with streams and bitrates that can be used by other methods @param proxy: Proxy - The proxy to use for opening @param url: String - The url to download Can be used like this: part = item.CreateNewEmptyMediaPart() for s, b in YouTube.GetStreamsFromYouTube(url, self.proxy): item.complete = True # s = self.GetVerifiableVideoUrl(s) part.AppendMediaStream(s, b) """ youTubeStreams = [] youTubeAddOnAvailable = xbmc.getCondVisibility( 'System.HasAddon("plugin.video.youtube")') == 1 if youTubeAddOnAvailable: Logger.Info("Found Youtube add-on. Using it") youTubeStreams.append((YouTube.__PlayYouTubeUrl(url), 0)) Logger.Trace(youTubeStreams) return youTubeStreams Logger.Info("No Kodi Youtube Video add-on was found. Falling back.") if "watch?v=" in url: videoId = url.split("?v=")[-1] Logger.Debug("Using Youtube ID '%s' retrieved from '%s'", videoId, url) # get the meta data url url = "http://www.youtube.com/get_video_info?hl=en_GB&asv=3&video_id=%s" % ( videoId, ) elif "get_video_info" not in url: Logger.Error("Invalid Youtube URL specified: '%s'", url) return url data = UriHandler.Open(url, proxy=proxy) # get the stream data from the page urlEncodedFmtStreamMap = Regexer.DoRegex( "url_encoded_fmt_stream_map=([^&]+)", data) urlEncodedFmtStreamMapData = HtmlEntityHelper.UrlDecode( urlEncodedFmtStreamMap[0]) # split per stream streams = urlEncodedFmtStreamMapData.split(',') for stream in streams: # let's create a new part qsData = dict([x.split("=") for x in stream.split("&")]) Logger.Trace(qsData) # get the stream encoding information from the iTag iTag = int(qsData.get('itag', -1)) streamEncoding = YouTube.__YouTubeEncodings.get(iTag, None) if streamEncoding is None: # if the iTag was not in the list, skip it. Logger.Debug( "Not using iTag %s as it is not in the list of supported encodings.", iTag) continue bitrate = streamEncoding[0] signature = qsData.get('sig', None) quality = qsData['quality'] videoUrl = HtmlEntityHelper.UrlDecode(qsData['url']) if signature is None: url = "%s&&quality=%s&ext=.%s" % (videoUrl, quality, streamEncoding[1]) else: url = "%s&signature=%s&quality=%s&ext=.%s" % ( videoUrl, signature, quality, streamEncoding[1]) youTubeStreams.append((url, bitrate)) return youTubeStreams
def CreateFolderItem(self, resultSet): """Creates a MediaItem of type 'folder' using the resultSet from the regex. Arguments: resultSet : tuple(strig) - the resultSet of the self.folderItemRegex Returns: A new MediaItem of type 'folder' This method creates a new MediaItem from the Regular Expression or Json results <resultSet>. The method should be implemented by derived classes and are specific to the channel. """ Logger.Trace(resultSet) matchedRegex = resultSet[0] resultSet = resultSet[1] if matchedRegex == 0: # Main regex match for the More Clips/Episodes folderId = resultSet["url"] folderType = resultSet["type"] # http://www.kijk.nl/ajax/qw/moreepisodes?format=wegmisbruikers&page=1&season=0&station=sbs6 if "ajax" in self.parentItem.url: # for ajax pages determine the next one and it's always a clip list or episode list folderNumber = int(self.parentItem.url.split("/")[-2]) folderNumber += 1 if "clip" in self.parentItem.url.lower(): title = "\bMeer clips" else: title = "\bMeer afleveringen" elif "clip" in folderType.lower(): # default clip start page = 1 title = "\bClips" folderNumber = 1 else: # default more episode page = 2 title = "\bMeer afleveringen" folderNumber = 2 url = "http://www.kijk.nl/ajax/section/series/%s/%s/%s" % ( folderId, folderNumber, self.pageSize) elif matchedRegex == 1: # match for the Seasons on the main pages. if "ajax" in self.parentItem.url: # don't add then om Ajax call backs, only on main listing return None title = resultSet["title"] url = resultSet["url"] url = "http://www.kijk.nl/ajax/section/series/%s/1/%s" % ( url, self.pageSize) else: Logger.Error("Unmatched multi regex match") return None item = mediaitem.MediaItem(title, url) item.thumb = self.noImage item.icon = self.icon item.type = 'folder' item.complete = True Logger.Trace(item) return item