de/generia/kodi/plugin/frontend/zdf/rubrics/ShowsAzPage.py
class ShowsAzPage(AbstractPage):

    def service(self, request, response):
        rubricBaseUrl = Constants.showsAzUrl + '?group='
        groups = []
        for i in range (0, 26):
            group = chr(ord('a') + i)
            groups.append(group)
        groups.append('0 - 9')
        
        for group in groups:
            url = rubricBaseUrl + group.replace(' ', '+')
            response.addFolder(self._(32038, group.upper()), Action('RubricPage', {'rubricUrl': url}))
de/generia/kodi/plugin/frontend/zdf/search/SearchHistory.py
class SearchHistory(object):
    entries = []
    storeFile = None
    searchHistorySize = None
    
    def __init__(self, log, storeFile, searchHistorySize):
        self.log = log
        self.storeFile = storeFile
        self.searchHistorySize = searchHistorySize
        self._checkStorePath()
                    
    def getEntries(self):
        entries = self._loadEntries()
        return entries
    
    def addEntry(self, entry):
        entries = self._loadEntries()
        
        found = False
        for e in entries:
            if e.query.lower() == entry.query.lower():
                e.query = entry.query
                e.date = entry.date
                e.contentTypes = entry.contentTypes
                found = True
                break
        
        if len(entries) > self.searchHistorySize-1:
            entries = entries[0:self.searchHistorySize-1]

        if not found:
            entries.append(entry)

        self._saveEntries(entries)

    def _loadEntries(self):
        entries = []
        try:
            with open(self.storeFile, "r") as lines:
                entries = []
                for line in lines:
                    entry = self._parseEntry(line)
                    if entry is not None:
                        entries.append(entry)
        except IOError as e:
            self.log.error(LOG_PREFIX + "caught exception while loading search history using store-file '{}', exception: {}", self.storeFile, e)
        return entries
                
    def _parseEntry(self, line):
        i = line.find(" ");
        if i == -1:
            return None        
        j = line.find(" ", i+1)
        if j == -1:
            return None        
        
        date = line[0:i]
        contentTypes = line[i+1:j]
        query = line[j+1:len(line)-1]
        
        if contentTypes == 'None':
            contentTypes = None
        #self.log.info(LOG_PREFIX + "_parseEntry: " + str(date) + "-" + query)
        return HistoryEntry(query, contentTypes, date)  

    def _saveEntries(self, entries):
        #self.log.info(LOG_PREFIX + "_saveEntries: " + str(len(entries)))
                 
        try:
            file = open(self.storeFile, "w")
            for entry in entries:
                date = entry.date #getDateFormatted()
                file.write('{0} {1} {2}\n'.format(date, entry.contentTypes, entry.query))
            file.close()
        except IOError as e:
            self.log.error(LOG_PREFIX + "caught exception while saving search history using store-file '{}', exception: {}", self.storeFile, e)

    def _checkStorePath(self):
        storeDir = os.path.dirname(self.storeFile)
        if os.path.exists(storeDir):
            return
        try:
            os.makedirs(storeDir)
        except IOError as e:
            self.log.error(LOG_PREFIX + "caught exception while creating store-dir '{}', exception: {}", storeDir, e)
            return
de/generia/kodi/plugin/frontend/zdf/rubrics/RubricsPage.py
class RubricsPage(AbstractPage):

    excludedRubricUrls = [
        Constants.showsAzUrl,
        '/bestbewertet',
        '/meist-gesehen',
        #'/barrierefreiheit-im-zdf'
    ]
    
    def service(self, request, response):

        navigation = NavigationResource(Constants.baseUrl)
        self._parse(navigation)

        for rubric in navigation.rubrics:
            if self._isExcluded(rubric):
                continue
            response.addFolder(str(self._(32004)) + ' - ' + rubric.title, Action('RubricPage', {'rubricUrl': rubric.url}))

    def _isExcluded(self, rubric):
        for url in self.excludedRubricUrls:
            if url == rubric.url:
                return True
        return False
de/generia/kodi/plugin/frontend/zdf/rubrics/RubricPage.py
class RubricPage(AbstractPage):

    def service(self, request, response):
        rubricUrl = request.getParam('rubricUrl')
        listType = request.getParam('listType')
        listStart = int(request.getParam('listStart', -1))
        listEnd = int(request.getParam('listEnd', -1))
        
        self.info("rubric-page: url='{}', listType='{}', listStart='{}', listEnd='{}'", rubricUrl, listType, listStart, listEnd)

        rubricResource = RubricResource(Constants.baseUrl + rubricUrl, listType, listStart, listEnd)
        self._parse(rubricResource)
        self._resolveLazyloadTeasers(rubricResource)
        
        if rubricResource.isRedirect:
            self.info("redirect detected to url='{}', skipping ...", rubricResource.responseLocation)
            dialog = xbmcgui.Dialog()
            dialog.ok(self._(32041), self._(32042, rubricUrl, rubricResource.responseLocation))
            return
        
        clusters = rubricResource.clusters
        
        if listType is not None:
            if len(clusters) == 1:
                self._renderTeasers(clusters[0].teasers, response)
            else:
                self.warn("can't find cluster of type '{}' in rubric-url '{}'", listType, rubricUrl)
        else:
            self._checkTeasers(rubricResource.teasers, rubricResource.configApiToken)
            self._renderTeasers(rubricResource.teasers, response)
            self._renderClusters(clusters, response, rubricUrl)
            
    
    def _resolveLazyloadTeasers(self, rubricResource):
        teaserLazyloadResolver = TeaserLazyloadResolver(self.log)
        for cluster in rubricResource.clusters:
            cluster.teasers.extend(teaserLazyloadResolver.resolveTeasers(cluster.lazyloadTeasers))
   
    def _renderClusters(self, clusters, response, rubricUrl):
        for cluster in clusters:
            clusterTitle = cluster.title #.encode('ascii', 'ignore')
            action = Action(pagelet='RubricPage', params={'rubricUrl': rubricUrl, 'listType': cluster.listType, 'listStart': str(cluster.listStart), 'listEnd': str(cluster.listEnd)})
            item = Item(cluster.title, action, cluster.image, isFolder=True)
            response.addItem(item)            
    
    
    def _checkTeasers(self, teasers, apiToken):
        for teaser in teasers:
            if teaser.image is None and teaser.contentName is not None:
                videoContentUrl = Constants.apiContentUrl + teaser.contentName + '.json?profile=player'
                self.debug("downloading video-content-url '{1}' ...", videoContentUrl)
                videoContent = VideoContentResource(videoContentUrl, Constants.apiBaseUrl, apiToken)
                self._parse(videoContent)
                teaser.image = videoContent.image
                    
    def _renderTeasers(self, teasers, response):
        for teaser in teasers:
            item = self._createItem(teaser)
            if item is not None:
                response.addItem(item)
            else:
                self.warn("can't find content-name for teaser-url '{}' and teaser-title '{}', skipping item ...", teaser.url, teaser.title)
de/generia/kodi/plugin/frontend/zdf/player/PlayVideo.py
class PlayVideo(Pagelet):
    
    def __init__(self, playerStore, filterMasterPlaylist, disableSubtitles):
        super(Pagelet, self).__init__()
        self.playerStore = playerStore
        self.filterMasterPlaylist = filterMasterPlaylist
        self.disableSubtitles = disableSubtitles

    def service(self, request, response):
        contentName = request.getParam('contentName')
        title = request.getParam('title')
        videoUrl = request.getParam('videoUrl')
        date = request.getParam('date')
        genre = request.getParam('genre')

        # get api-token handling parameters
        self.videoUrl = request.getParam('videoUrl')
        self.apiToken = request.getParam('apiToken')
        if self.apiToken is None:
            self.apiToken = self.playerStore.getApiToken()
            if self.apiToken is None:
                self._refreshApiToken()
            
        item = None
        if contentName is not None:
            try:
                dialog = xbmcgui.DialogProgressBG()
                dialog.create(self._(32007), self._(32008))
                videoContentUrl = Constants.apiContentUrl + contentName + '.json?profile=player2'
                self.debug("downloading video-content-url '{1}' ...", videoContentUrl)
                videoContent = self._getVideoContent(videoContentUrl)
                
                if videoContent.streamInfoUrl is None:
                    self.warn("can't find stream-info-url in video-content '{1}' in content '{2}'", contentName, videoContent.content)
                    response.sendError(self._(32011) + " '" + contentName +"'", Action('SearchPage'))
                    return
            
                dialog.update(percent=50, message=self._(32009))
                self.debug("downloading stream-info-url '{1}' ...", videoContent.streamInfoUrl)
                streamInfo = self._getStreamInfo(videoContent.streamInfoUrl)
                
                rawPlaylistUrl = streamInfo.streamUrl
                if rawPlaylistUrl is None:
                    self.warn("can't find stream-url in stream-info-url '{1}' in content '{2}'", videoContent.streamInfoUrl, streamInfo.content)
                    response.sendError(self._(32012) + " '" + contentName +"'", Action('SearchPage'))
                    return

                # finding best stream url
                dialog.update(percent=70, message=self._(32044))
                self.debug("downloading master playlist '{1}' ...", rawPlaylistUrl)
                url = self._getPlaylistUrl(rawPlaylistUrl)
                
                title = videoContent.title
                text = videoContent.text
                infoLabels = {}
                infoLabels['title'] = title
                infoLabels['sorttitle'] = title
                infoLabels['genre'] = genre
                #infoLabels['plot'] = text
                #infoLabels['plotoutline'] = text
                infoLabels['tvshowtitle'] = title
                infoLabels['tagline'] = text
                if videoContent.duration is not None:
                    infoLabels['duration'] = videoContent.duration
                infoLabels['mediatype'] = 'video'

                if date is not None and date != "":
                    infoLabels['date'] = date
        
                item = xbmcgui.ListItem(title, text)
                item.setPath(url)
                item.setInfo(type="Video", infoLabels=infoLabels)
                image = videoContent.image
                if image is not None:
                    item.setArt({'poster': image, 'banner': image, 'thumb': image, 'icon': image, 'fanart': image})

                # set subtitles
                if not self.disableSubtitles:
                    self._setSubTitles(item, streamInfo.subTitlesUrl)
                
                dialog.update(percent=90, message=self._(32010))
                self.info("setting resolved url='{1}' ...", url)
                xbmcplugin.setResolvedUrl(response.handle, True, item)
            finally:
                dialog.close();
            
    def _getStreamInfo(self, streamInfoUrl):
            streamInfo = None
            try:
                streamInfo = StreamInfoResource(streamInfoUrl, self.apiToken)
                self._parse(streamInfo)
            except HTTPError as e:
                # check for forbidden, caused by wrong api-token
                if e.code != 403:
                    raise e
                
                # try again with more specific video api-token
                if not self._refreshApiToken():
                    raise e

                streamInfo = StreamInfoResource(streamInfoUrl, self.apiToken)
                self._parse(streamInfo)
            
            return streamInfo
            
    def _getVideoContent(self, videoContentUrl):
            videoContent = None
            try:
                videoContent = VideoContentResource(videoContentUrl, Constants.apiBaseUrl, self.apiToken)
                self._parse(videoContent)
            except HTTPError as e:
                # check for forbidden, caused by wrong api-token
                if e.code != 403:
                    raise e
                
                # try again with more specific video api-token
                if not self._refreshApiToken():
                    raise e
                
                videoContent = VideoContentResource(videoContentUrl, Constants.apiBaseUrl, self.apiToken)
                self._parse(videoContent)
            
            return videoContent

    def _refreshApiToken(self):
        if self.videoUrl is None:
            self.error("can't refresh api-token: videoUrl is not set")
            return False
        
        video = VideoResource(Constants.baseUrl + self.videoUrl)
        self._parse(video)
        apiToken = video.apiToken
        self.playerStore.setApiToken(apiToken)
        self.apiToken = apiToken
        return True
    
    def _setSubTitles(self, item, subTitlesUrl):            
        if subTitlesUrl is not None:
            try:
                item.addStreamInfo('subtitle', {'language': 'de'})
                item.setSubtitles([subTitlesUrl])
                self.info("setting sub-titles-url='{1}' ...", subTitlesUrl)
            except AttributeError:
                self.info("no sub-titles supported before Kodi 14.x 'Helix', skipping subtitles ...")
        else:
            self.info("no sub-titles-url available in stream-info, skipping subtitles ...")
 
    def _getPlaylistUrl(self, rawPlaylistUrl):
        if not self.filterMasterPlaylist:
            return rawPlaylistUrl
        
        masterResource = PlaylistResource(rawPlaylistUrl)
        try:
            self._parse(masterResource)
        except HTTPError as e:
            self.warn("could not load master playlist '{1}', falling back to raw playlist...", rawPlaylistUrl)
            return rawPlaylistUrl

        playlist = masterResource.getPlaylist()
        if playlist is not None:
            self.playerStore.storePlaylist(playlist)
            return self.playerStore.getPlaylistUrl()
        self.warn("could not filter playlist '{1}', falling back to raw playlist...", rawPlaylistUrl)
        return rawPlaylistUrl
de/generia/kodi/plugin/frontend/zdf/rubrics/LiveTvPage.py
class LiveTvPage(AbstractPage):

    def service(self, request, response):
        liveTvUrl = Constants.baseUrl + '/live-tv'
        liveTvResource = LiveTvResource(liveTvUrl)
        self._parse(liveTvResource)

        for teaser in liveTvResource.teasers:
            item = self._createItem(teaser)
            if item is not None:
                response.addItem(item)
de/generia/kodi/plugin/frontend/zdf/search/SearchPage.py
class SearchPage(AbstractPage):
    searchHistory = None
    
    def __init__(self, searchHistory):
        super(SearchPage, self).__init__()
        self.searchHistory = searchHistory

    def service(self, request, response):
        pages = int(request.getParam('pages', -1))
        page = int(request.getParam('page', 1))
        
        query = dict(request.params)
        if 'pages' in query:
            del query['pages']
        
        query['from'] = ''
        query['to'] = ''
        query['sender'] = 'alle Sender'
        query['attrs'] = ''
        
        if 'q' not in query:
            self.info("Timer - getting search-string from keyboard ...")
            start = self.context.log.start()
            text = self._getKeyboardInput()
            self.info("Timer - getting search-string from keyboard ... done. [{} ms]", self.context.log.stop(start))
            if text is not None:
                query['q'] = text
            else:
                response.sendInfo(self._(32006))
                return

        self.info("Timer - loading results  ...")
        start = self.context.log.start()
        self._progress = xbmcgui.DialogProgress()
        try:
            msg = self._(32021)
            if pages != -1:
                msg = self._(32022, page, pages)
            self._progress.create(self._(32020), msg)
            self._progress.update(0, msg)
            self._loadResults(request, response, pages, page, query)
            
            # add search history entry
            self._saveQuery(query)
            
        #except:
        #    self.warn("Timer - loading results ... exception")            
        finally:
            self.info("Timer - loading results ... done. [{} ms]", self.context.log.stop(start))
            self._progress.close();

    def _saveQuery(self, query):
        if self.results > 0:
            contentTypes = None
            if 'contentTypes' in query:
                contentTypes = query['contentTypes']
            self.searchHistory.addEntry(HistoryEntry(query['q'].strip(), contentTypes))

    def _loadResults(self, request, response, pages, page, query):
        queryParams = urllib.urlencode(query)
        searchUrl = Constants.baseUrl + "/suche?" + queryParams
        
        self.info("searching url: '{}' ...", searchUrl)
        searchPage = SearchResource(searchUrl)
        self._parse(searchPage)
        self.info("found '{}' results.", len(searchPage.teasers))

        if len(searchPage.teasers) == 0:
            response.sendInfo(self._(32013))
        
        pages = int(math.ceil(float(searchPage.results) / float(searchPage.resultsPerPage)))
        
        self.results = 0
        self._addItems(response, searchPage.teasers)
        
        if len(searchPage.teasers) == 0:
            return
        
        if self.settings.loadAllSearchResults:
            self._addMoreResults(response, searchPage.moreUrl, pages, page)
        else:
            self._addMoreFolder(response, searchPage.moreUrl, pages, page)
            
        self._progress.update(percent=100)
        self.info("added '{}' result-items.", self.results)


    def _addItems(self, response, teasers):
        self.debug("Timer - creating list items  ...")
        start = self.context.log.start()
        for teaser in teasers:
            if not self.settings.showOnlyPlayableSearchResults or teaser.playable: 
                item = self._createItem(teaser)
                response.addItem(item)
                self.results += 1
        self.debug("Timer - creating list items ... done. [{} ms]", self.context.log.stop(start))

    def _addMoreResults(self, response, moreUrl, pages, page):

        while moreUrl is not None and page < pages and not self._progress.iscanceled():
            moreUrl = moreUrl.replace('&#x3D;', '=')
            moreUrl = moreUrl.replace('&amp;', '&')

            page += 1
            percent = page*100/pages
            self._progress.update(percent, self._(32022, page, pages))

            searchUrl = Constants.baseUrl + moreUrl
            self.info("searching url: '{}' ...", searchUrl)
            searchPage = SearchResource(searchUrl)
            self._parse(searchPage)
            
            if len(searchPage.teasers) > 0:
                self._addItems(response, searchPage.teasers)
                moreUrl = searchPage.moreUrl
            else: 
                moreUrl = None
            self.info("found '{}' results.", len(searchPage.teasers))

    def _addMoreFolder(self, response, moreUrl, pages, page):
        if page < pages:                
            page += 1
            moreAction = self._getMoreAction(moreUrl, pages, page)
            response.addFolder(self._(32017, page, pages), moreAction)

    def _getMoreAction(self, moreUrl, pages, page):
        i = moreUrl.find('?')
        if i != -1:
            moreQuery = moreUrl[i+1:]
            moreQuery = moreQuery.replace('&#x3D;', '=')
            moreQuery = moreQuery.replace('&amp;', '&')
            searchArgs = urlparse.parse_qs(moreQuery)
            for key, value in searchArgs.iteritems():
                searchArgs[key] = value[0]
            searchArgs['pages'] = pages
            searchArgs['page'] = page
            moreAction = Action('SearchPage', searchArgs)
            return moreAction
        

    def _getKeyboardInput(self):
        keyboard = xbmc.Keyboard('', self._(32005))
        keyboard.doModal()
        text = None
        if keyboard.isConfirmed() and keyboard.getText():
            text = keyboard.getText()
        return text
de/generia/kodi/plugin/frontend/zdf/Mediathek.py
class Mediathek(Pagelet):

    def service(self, request, response):
        response.addFolder(self._(32005), Action('SearchMenuPage'))

        response.addFolder(self._(32003), Action('RubricsPage'))

        response.addFolder(self._(32043), Action('RubricPage', {'rubricUrl': '/'}))
        response.addFolder(self._(32032), Action('RubricPage', {'rubricUrl': '/meist-gesehen'}))
        response.addFolder(self._(32037), Action('ShowsAzPage'))
        #response.addFolder(self._(32034), Action('RubricPage', {'rubricUrl': '/barrierefreiheit-im-zdf'}))
        response.addFolder(self._(32036), Action('RubricPage', {'rubricUrl': '/sendung-verpasst'}))

        response.addFolder(self._(32035), Action('LiveTvPage'))
de/generia/kodi/plugin/frontend/zdf/Constants.py
class Constants(object):
    baseUrl = 'https://www.zdf.de'
    apiBaseUrl = 'https://api.zdf.de'
    apiContentUrl = apiBaseUrl + '/content/documents'
    showsAzUrl = '/sendungen-a-z'
de/generia/kodi/plugin/frontend/base/PageletFactory.py
class PageletFactory(object):

    def __init__(self, log):
        self.log = log
                 
    def createPagelet(self, context, pageletId, params):
        pass
de/generia/kodi/plugin/frontend/zdf/search/SearchMenuPage.py
class SearchMenuPage(AbstractPage):

    def service(self, request, response):
        response.addFolder(self._(32030), Action('SearchHistoryPage'))
        response.addFolder(self._(32001), Action('SearchPage', {'contentTypes':'episode'}))
        response.addFolder(self._(32002), Action('SearchPage'))
de/generia/kodi/plugin/frontend/zdf/player/PlayerStore.py
class PlayerStore(object):
    apiTokenFile = None
    
    def __init__(self, log, apiTokenFile, playlistFile):
        self.log = log
        self.apiTokenFile = apiTokenFile
        self.playlistFile = playlistFile
        self._checkStorePath()
                    
    def getApiToken(self):
        apiToken = None
        try:
            with open(self.apiTokenFile, "r") as lines:
                for line in lines:
                    apiToken = line.strip()
                    self.log.info(LOG_PREFIX + "loading api-token '{}' from store-file '{}'", apiToken, self.apiTokenFile)
                    break
        except IOError as e:
            self.log.error(LOG_PREFIX + "caught exception while loading api-token using store-file '{}', exception: {}", self.apiTokenFile, e)
            
        return apiToken

    def setApiToken(self, apiToken):
        self.log.info(LOG_PREFIX + "saving api-token '{}' to store-file '{}'", apiToken, self.apiTokenFile)
        try:
            file = open(self.apiTokenFile, "w")
            if apiToken is not None:
                file.write(apiToken)
            file.close()
        except IOError as e:
            self.log.error(LOG_PREFIX + "caught exception while saving api-token using store-file '{}', exception: {}", self.apiTokenFile, e)
                    
    def getPlaylistUrl(self):
        
        return self.playlistFile

    def storePlaylist(self, playlist):
        self.log.info(LOG_PREFIX + "saving playlist to store-file '{}'", self.playlistFile)
        try:
            file = open(self.playlistFile, "w")
            file.write(playlist)
            file.close()
        except IOError as e:
            self.log.error(LOG_PREFIX + "caught exception while saving playlist using store-file '{}', exception: {}", self.playlistFile, e)

    def _checkStorePath(self):
        storeDir = os.path.dirname(self.apiTokenFile)
        if os.path.exists(storeDir):
            return
        try:
            os.makedirs(storeDir)
        except IOError as e:
            self.log.error(LOG_PREFIX + "caught exception while creating store-dir '{}', exception: {}", storeDir, e)
            return
de/generia/kodi/plugin/frontend/zdf/search/SearchHistoryPage.py
class SearchHistoryPage(AbstractPage):

    def __init__(self, searchHistory):
        super(SearchHistoryPage, self).__init__()
        self.searchHistory = searchHistory

    def service(self, request, response):
        entries = self.searchHistory.getEntries()
        
        for entry in entries:
            params = {'q': entry.query }
            if entry.contentTypes is not None:
                params['contentTypes'] = entry.contentTypes
                
            response.addFolder(self._(32029, entry.date, entry.query), Action('SearchPage', params), date=entry.date)


Question: import os
import xbmc
from xbmcaddon import Addon
from de.generia.kodi.plugin.frontend.base.PageletFactory import PageletFactory        
from de.generia.kodi.plugin.frontend.zdf.Constants import Constants
from de.generia.kodi.plugin.frontend.zdf.Mediathek import Mediathek
from de.generia.kodi.plugin.frontend.zdf.player.PlayVideo import PlayVideo        
from de.generia.kodi.plugin.frontend.zdf.player.PlayerStore import PlayerStore        
from de.generia.kodi.plugin.frontend.zdf.rubrics.LiveTvPage import LiveTvPage       
from de.generia.kodi.plugin.frontend.zdf.rubrics.RubricPage import RubricPage       
from de.generia.kodi.plugin.frontend.zdf.rubrics.RubricsPage import RubricsPage       
from de.generia.kodi.plugin.frontend.zdf.rubrics.ShowsAzPage import ShowsAzPage       
from de.generia.kodi.plugin.frontend.zdf.search.SearchPage import SearchPage       
from de.generia.kodi.plugin.frontend.zdf.search.SearchMenuPage import SearchMenuPage       
from de.generia.kodi.plugin.frontend.zdf.search.SearchHistory import SearchHistory       
from de.generia.kodi.plugin.frontend.zdf.search.SearchHistoryPage import SearchHistoryPage       









class MediathekFactory(PageletFactory):
    settings = None
    
    def __init__(self, log, settings=None):
        super(MediathekFactory, self).__init__(log)
        self.settings = settings
        
    def createPagelet(self, context, pageletId, params):
        if pageletId == 'SearchPage':
            return SearchPage(self._createSearchHistory(context))
        if pageletId == 'SearchMenuPage':

Answer:             return SearchMenuPage()