class MediathekView(KodiPlugin):
    def __init__(self):
        super(MediathekView, self).__init__()
        self.settings = Settings()
        self.notifier = Notifier()
        self.database = Store(self.getNewLogger('Store'), self.notifier,
                              self.settings)

    def show_main_menu(self):
        # Search
        self.addFolderItem(30901, {'mode': "search", 'extendedsearch': False})
        # Search all
        self.addFolderItem(30902, {'mode': "search", 'extendedsearch': True})
        # Browse livestreams
        self.addFolderItem(30903, {'mode': "livestreams"})
        # Browse recently added
        self.addFolderItem(30904, {'mode': "recent", 'channel': 0})
        # Browse recently added by channel
        self.addFolderItem(30905, {'mode': "recentchannels"})
        # Browse by Initial->Show
        self.addFolderItem(30906, {'mode': "initial", 'channel': 0})
        # Browse by Channel->Initial->Shows
        self.addFolderItem(30907, {'mode': "channels"})
        # Database Information
        self.addActionItem(30908, {'mode': "action-dbinfo"})
        # Manual database update
        if self.settings.updmode == 1 or self.settings.updmode == 2:
            self.addActionItem(30909, {'mode': "action-dbupdate"})
        self.endOfDirectory()
        self._check_outdate()

    def show_searches(self, extendedsearch=False):
        self.addFolderItem(30931, {
            'mode': "newsearch",
            'extendedsearch': extendedsearch
        })
        RecentSearches(self, extendedsearch).load().populate()
        self.endOfDirectory()

    def new_search(self, extendedsearch=False):
        settingid = 'lastsearch2' if extendedsearch is True else 'lastsearch1'
        headingid = 30902 if extendedsearch is True else 30901
        # are we returning from playback ?
        search = self.addon.getSetting(settingid)
        if search:
            # restore previous search
            self.database.Search(search, FilmUI(self), extendedsearch)
        else:
            # enter search term
            (search, confirmed) = self.notifier.GetEnteredText('', headingid)
            if len(search) > 2 and confirmed is True:
                RecentSearches(self, extendedsearch).load().add(search).save()
                if self.database.Search(search, FilmUI(self),
                                        extendedsearch) > 0:
                    self.addon.setSetting(settingid, search)
            else:
                # pylint: disable=line-too-long
                self.info(
                    'The following ERROR can be ignored. It is caused by the architecture of the Kodi Plugin Engine'
                )
                self.endOfDirectory(False, cacheToDisc=True)
                # self.show_searches( extendedsearch )

    def show_db_info(self):
        info = self.database.GetStatus()
        heading = self.language(30907)
        infostr = self.language({
            'NONE': 30941,
            'UNINIT': 30942,
            'IDLE': 30943,
            'UPDATING': 30944,
            'ABORTED': 30945
        }.get(info['status'], 30941))
        infostr = self.language(30965) % infostr
        totinfo = self.language(30971) % (info['tot_chn'], info['tot_shw'],
                                          info['tot_mov'])
        updatetype = self.language(30972 if info['fullupdate'] > 0 else 30973)
        if info['status'] == 'UPDATING' and info['filmupdate'] > 0:
            updinfo = self.language(30967) % (
                updatetype, datetime.datetime.fromtimestamp(
                    info['filmupdate']).strftime('%Y-%m-%d %H:%M:%S'),
                info['add_chn'], info['add_shw'], info['add_mov'])
        elif info['status'] == 'UPDATING':
            updinfo = self.language(30968) % (updatetype, info['add_chn'],
                                              info['add_shw'], info['add_mov'])
        elif info['lastupdate'] > 0 and info['filmupdate'] > 0:
            updinfo = self.language(30969) % (
                updatetype, datetime.datetime.fromtimestamp(
                    info['lastupdate']).strftime('%Y-%m-%d %H:%M:%S'),
                datetime.datetime.fromtimestamp(
                    info['filmupdate']).strftime('%Y-%m-%d %H:%M:%S'),
                info['add_chn'], info['add_shw'], info['add_mov'],
                info['del_chn'], info['del_shw'], info['del_mov'])
        elif info['lastupdate'] > 0:
            updinfo = self.language(30970) % (
                updatetype, datetime.datetime.fromtimestamp(
                    info['lastupdate']).strftime('%Y-%m-%d %H:%M:%S'),
                info['add_chn'], info['add_shw'], info['add_mov'],
                info['del_chn'], info['del_shw'], info['del_mov'])
        else:
            updinfo = self.language(30966)

        xbmcgui.Dialog().textviewer(
            heading, infostr + '\n\n' + totinfo + '\n\n' + updinfo)

    def _check_outdate(self, maxage=172800):
        if self.settings.updmode != 1 and self.settings.updmode != 2:
            # no check with update disabled or update automatic
            return
        if self.database is None:
            # should never happen
            self.notifier.ShowOutdatedUnknown()
            return
        status = self.database.GetStatus()
        if status['status'] == 'NONE' or status['status'] == 'UNINIT':
            # should never happen
            self.notifier.ShowOutdatedUnknown()
            return
        elif status['status'] == 'UPDATING':
            # great... we are updating. nuthin to show
            return
        # lets check how old we are
        tsnow = int(time.time())
        tsold = int(status['lastupdate'])
        if tsnow - tsold > maxage:
            self.notifier.ShowOutdatedKnown(status)

    def init(self):
        if self.database.Init():
            if self.settings.HandleFirstRun():
                pass
            self.settings.HandleUpdateOnStart()

    def run(self):
        # save last activity timestamp
        self.settings.ResetUserActivity()
        # process operation
        mode = self.get_arg('mode', None)
        if mode is None:
            self.show_main_menu()
        elif mode == 'search':
            extendedsearch = self.get_arg('extendedsearch', 'False') == 'True'
            self.show_searches(extendedsearch)
        elif mode == 'newsearch':
            self.new_search(self.get_arg('extendedsearch', 'False') == 'True')
        elif mode == 'research':
            search = self.get_arg('search', '')
            extendedsearch = self.get_arg('extendedsearch', 'False') == 'True'
            self.database.Search(search, FilmUI(self), extendedsearch)
            RecentSearches(self, extendedsearch).load().add(search).save()
        elif mode == 'delsearch':
            search = self.get_arg('search', '')
            extendedsearch = self.get_arg('extendedsearch', 'False') == 'True'
            RecentSearches(
                self, extendedsearch).load().delete(search).save().populate()
            self.runBuiltin('Container.Refresh')
        elif mode == 'livestreams':
            self.database.GetLiveStreams(
                FilmUI(self, [xbmcplugin.SORT_METHOD_LABEL]))
        elif mode == 'recent':
            channel = self.get_arg('channel', 0)
            self.database.GetRecents(channel, FilmUI(self))
        elif mode == 'recentchannels':
            self.database.GetRecentChannels(ChannelUI(self, nextdir='recent'))
        elif mode == 'channels':
            self.database.GetChannels(ChannelUI(self, nextdir='shows'))
        elif mode == 'action-dbinfo':
            self.show_db_info()
        elif mode == 'action-dbupdate':
            self.settings.TriggerUpdate()
            self.notifier.ShowNotification(30963, 30964, time=10000)
        elif mode == 'initial':
            channel = self.get_arg('channel', 0)
            self.database.GetInitials(channel, InitialUI(self))
        elif mode == 'shows':
            channel = self.get_arg('channel', 0)
            initial = self.get_arg('initial', None)
            self.database.GetShows(channel, initial, ShowUI(self))
        elif mode == 'films':
            show = self.get_arg('show', 0)
            self.database.GetFilms(show, FilmUI(self))
        elif mode == 'downloadmv':
            filmid = self.get_arg('id', 0)
            quality = self.get_arg('quality', 1)
            Downloader(self).download_movie(filmid, quality)
        elif mode == 'downloadep':
            filmid = self.get_arg('id', 0)
            quality = self.get_arg('quality', 1)
            Downloader(self).download_episode(filmid, quality)
        elif mode == 'playwithsrt':
            filmid = self.get_arg('id', 0)
            only_sru = self.get_arg('only_set_resolved_url', 'False') == 'True'
            Downloader(self).play_movie_with_subs(filmid, only_sru)

        # cleanup saved searches
        if mode is None or mode != 'search':
            self.addon.setSetting('lastsearch1', '')
        if mode is None or mode != 'searchall':
            self.addon.setSetting('lastsearch2', '')

    def exit(self):
        self.database.Exit()
class MediathekViewUpdater(object):
    def __init__(self, logger, notifier, settings, monitor=None):
        self.logger = logger
        self.notifier = notifier
        self.settings = settings
        self.monitor = monitor
        self.db = None
        self.cycle = 0
        self.use_xz = mvutils.find_xz() is not None

    def Init(self, convert=False):
        if self.db is not None:
            self.Exit()
        self.db = Store(self.logger, self.notifier, self.settings)
        self.db.Init(convert=convert)

    def Exit(self):
        if self.db is not None:
            self.db.Exit()
            del self.db
            self.db = None

    def Reload(self):
        self.Exit()
        self.Init()

    def IsEnabled(self):
        return self.settings.updenabled

    def GetCurrentUpdateOperation(self, force=False):
        if self.db is None:
            # db not available - no update
            self.logger.info('Update disabled since database not available')
            return 0

        elif self.settings.updmode == 0:
            # update disabled - no update
            return 0

        elif self.settings.updmode == 1 or self.settings.updmode == 2:
            # manual update or update on first start
            if self.settings.IsUpdateTriggered() is True:
                return self._getNextUpdateOperation(True)
            else:
                # no update on all subsequent calls
                return 0

        elif self.settings.updmode == 3:
            # automatic update
            if self.settings.IsUserAlive():
                return self._getNextUpdateOperation(force)
            else:
                # no update of user is idle for more than 2 hours
                return 0

    def _getNextUpdateOperation(self, force=False):
        status = self.db.GetStatus()
        tsnow = int(time.time())
        tsold = status['lastupdate']
        dtnow = datetime.datetime.fromtimestamp(tsnow).date()
        dtold = datetime.datetime.fromtimestamp(tsold).date()
        if status['status'] == 'UNINIT':
            # database not initialized - no update
            self.logger.debug('database not initialized')
            return 0
        elif status['status'] == "UPDATING" and tsnow - tsold > 10800:
            # process was probably killed during update - no update
            self.logger.info(
                'Stuck update pretending to run since epoch {} reset', tsold)
            self.db.UpdateStatus('ABORTED')
            return 0
        elif status['status'] == "UPDATING":
            # already updating - no update
            self.logger.debug('Already updating')
            return 0
        elif not force and tsnow - tsold < self.settings.updinterval:
            # last update less than the configured update interval - no update
            self.logger.debug(
                'Last update less than the configured update interval. do nothing'
            )
            return 0
        elif dtnow != dtold:
            # last update was not today. do full update once a day
            self.logger.debug(
                'Last update was not today. do full update once a day')
            return 1
        elif status['status'] == "ABORTED" and status['fullupdate'] == 1:
            # last full update was aborted - full update needed
            self.logger.debug(
                'Last full update was aborted - full update needed')
            return 1
        else:
            # do differential update
            self.logger.debug('Do differential update')
            return 2

    def Update(self, full):
        if self.db is None:
            return
        if self.db.SupportsUpdate():
            if self.GetNewestList(full):
                if self.Import(full):
                    self.cycle += 1
            self.DeleteList(full)

    def Import(self, full):
        (_, _, destfile, avgrecsize) = self._get_update_info(full)
        if not mvutils.file_exists(destfile):
            self.logger.error('File {} does not exists', destfile)
            return False
        # estimate number of records in update file
        records = int(mvutils.file_size(destfile) / avgrecsize)
        if not self.db.ftInit():
            self.logger.warn(
                'Failed to initialize update. Maybe a concurrency problem?')
            return False
        try:
            self.logger.info('Starting import of approx. {} records from {}',
                             records, destfile)
            with open(destfile, 'r') as file:
                parser = ijson.parse(file)
                flsm = 0
                flts = 0
                (self.tot_chn, self.tot_shw,
                 self.tot_mov) = self._update_start(full)
                self.notifier.ShowUpdateProgress()
                for prefix, event, value in parser:
                    if (prefix, event) == ("X", "start_array"):
                        self._init_record()
                    elif (prefix, event) == ("X", "end_array"):
                        self._end_record(records)
                        if self.count % 100 == 0 and self.monitor.abortRequested(
                        ):
                            # kodi is shutting down. Close all
                            self._update_end(full, 'ABORTED')
                            self.notifier.CloseUpdateProgress()
                            return True
                    elif (prefix, event) == ("X.item", "string"):
                        if value is not None:
                            self._add_value(value.strip())
                        else:
                            self._add_value("")
                    elif (prefix, event) == ("Filmliste", "start_array"):
                        flsm += 1
                    elif (prefix, event) == ("Filmliste.item", "string"):
                        flsm += 1
                        if flsm == 2 and value is not None:
                            # this is the timestmap of this database update
                            try:
                                fldt = datetime.datetime.strptime(
                                    value.strip(), "%d.%m.%Y, %H:%M")
                                flts = int(time.mktime(fldt.timetuple()))
                                self.db.UpdateStatus(filmupdate=flts)
                                self.logger.info('Filmliste dated {}',
                                                 value.strip())
                            except TypeError:
                                # SEE: https://forum.kodi.tv/showthread.php?tid=112916&pid=1214507#pid1214507
                                # Wonderful. His name is also Leopold
                                try:
                                    flts = int(
                                        time.mktime(
                                            time.strptime(
                                                value.strip(),
                                                "%d.%m.%Y, %H:%M")))
                                    self.db.UpdateStatus(filmupdate=flts)
                                    self.logger.info('Filmliste dated {}',
                                                     value.strip())
                                except Exception as err:
                                    # If the universe hates us...
                                    self.logger.debug(
                                        'Could not determine date "{}" of filmliste: {}',
                                        value.strip(), err)
                            except ValueError as err:
                                pass

            self.db.ftFlushInsert()
            self._update_end(full, 'IDLE')
            self.logger.info('Import of {} in update cycle {} finished',
                             destfile, self.cycle)
            self.notifier.CloseUpdateProgress()
            return True
        except KeyboardInterrupt:
            self._update_end(full, 'ABORTED')
            self.logger.info('Update cycle {} interrupted by user', self.cycle)
            self.notifier.CloseUpdateProgress()
            return False
        except DatabaseCorrupted as err:
            self.logger.error('{} on update cycle {}', err, self.cycle)
            self.notifier.CloseUpdateProgress()
        except DatabaseLost as err:
            self.logger.error('{} on update cycle {}', err, self.cycle)
            self.notifier.CloseUpdateProgress()
        except Exception as err:
            self.logger.error(
                'Error {} while processing {} on update cycle {}', err,
                destfile, self.cycle)
            self._update_end(full, 'ABORTED')
            self.notifier.CloseUpdateProgress()
        return False

    def GetNewestList(self, full):
        (url, compfile, destfile, _) = self._get_update_info(full)
        if url is None:
            self.logger.error(
                'No suitable archive extractor available for this system')
            self.notifier.ShowMissingExtractorError()
            return False

        # get mirrorlist
        self.logger.info('Opening {}', url)
        try:
            data = urllib2.urlopen(url).read()
        except urllib2.URLError as err:
            self.logger.error('Failure opening {}', url)
            self.notifier.ShowDownloadError(url, err)
            return False
        root = etree.fromstring(data)
        urls = []
        for server in root.findall('Server'):
            try:
                URL = server.find('URL').text
                Prio = server.find('Prio').text
                urls.append((self._get_update_url(URL),
                             float(Prio) + random.random() * 1.2))
                self.logger.info('Found mirror {} (Priority {})', URL, Prio)
            except AttributeError:
                pass
        urls = sorted(urls, key=itemgetter(1))
        urls = [url[0] for url in urls]

        # cleanup downloads
        self.logger.info('Cleaning up old downloads...')
        mvutils.file_remove(compfile)
        mvutils.file_remove(destfile)

        # download filmliste
        self.notifier.ShowDownloadProgress()
        lasturl = ''
        for url in urls:
            try:
                lasturl = url
                self.logger.info('Trying to download {} from {}...',
                                 os.path.basename(compfile), url)
                self.notifier.UpdateDownloadProgress(0, url)
                mvutils.url_retrieve(
                    url,
                    filename=compfile,
                    reporthook=self.notifier.HookDownloadProgress,
                    aborthook=self.monitor.abortRequested)
                break
            except urllib2.URLError as err:
                self.logger.error('Failure downloading {}', url)
                self.notifier.CloseDownloadProgress()
                self.notifier.ShowDownloadError(lasturl, err)
                return False
            except ExitRequested as err:
                self.logger.error(
                    'Immediate exit requested. Aborting download of {}', url)
                self.notifier.CloseDownloadProgress()
                self.notifier.ShowDownloadError(lasturl, err)
                return False
            except Exception as err:
                self.logger.error('Failure writng {}', url)
                self.notifier.CloseDownloadProgress()
                self.notifier.ShowDownloadError(lasturl, err)
                return False

        # decompress filmliste
        if self.use_xz is True:
            self.logger.info('Trying to decompress xz file...')
            retval = subprocess.call([mvutils.find_xz(), '-d', compfile])
            self.logger.info('Return {}', retval)
        elif upd_can_bz2 is True:
            self.logger.info('Trying to decompress bz2 file...')
            retval = self._decompress_bz2(compfile, destfile)
            self.logger.info('Return {}', retval)
        elif upd_can_gz is True:
            self.logger.info('Trying to decompress gz file...')
            retval = self._decompress_gz(compfile, destfile)
            self.logger.info('Return {}', retval)
        else:
            # should nebver reach
            pass

        self.notifier.CloseDownloadProgress()
        return retval == 0 and mvutils.file_exists(destfile)

    def DeleteList(self, full):
        (_, compfile, destfile, _) = self._get_update_info(full)
        self.logger.info('Cleaning up downloads...')
        mvutils.file_remove(compfile)
        mvutils.file_remove(destfile)

    def _get_update_info(self, full):
        if self.use_xz is True:
            ext = 'xz'
        elif upd_can_bz2 is True:
            ext = 'bz2'
        elif upd_can_gz is True:
            ext = 'gz'
        else:
            return (
                None,
                None,
                None,
                0,
            )

        if full:
            return (
                FILMLISTE_AKT_URL,
                os.path.join(self.settings.datapath, 'Filmliste-akt.' + ext),
                os.path.join(self.settings.datapath, 'Filmliste-akt'),
                600,
            )
        else:
            return (
                FILMLISTE_DIF_URL,
                os.path.join(self.settings.datapath, 'Filmliste-diff.' + ext),
                os.path.join(self.settings.datapath, 'Filmliste-diff'),
                700,
            )

    def _get_update_url(self, url):
        if self.use_xz is True:
            return url
        elif upd_can_bz2 is True:
            return os.path.splitext(url)[0] + '.bz2'
        elif upd_can_gz is True:
            return os.path.splitext(url)[0] + '.gz'
        else:
            # should never happen since it will not be called
            return None

    def _update_start(self, full):
        self.logger.info('Initializing update...')
        self.add_chn = 0
        self.add_shw = 0
        self.add_mov = 0
        self.del_chn = 0
        self.del_shw = 0
        self.del_mov = 0
        self.index = 0
        self.count = 0
        self.film = {
            "channel": "",
            "show": "",
            "title": "",
            "aired": "1980-01-01 00:00:00",
            "duration": "00:00:00",
            "size": 0,
            "description": "",
            "website": "",
            "url_sub": "",
            "url_video": "",
            "url_video_sd": "",
            "url_video_hd": "",
            "airedepoch": 0,
            "geo": ""
        }
        return self.db.ftUpdateStart(full)

    def _update_end(self, full, status):
        self.logger.info('Added: channels:%d, shows:%d, movies:%d ...' %
                         (self.add_chn, self.add_shw, self.add_mov))
        (self.del_chn, self.del_shw, self.del_mov, self.tot_chn, self.tot_shw,
         self.tot_mov) = self.db.ftUpdateEnd(full and status == 'IDLE')
        self.logger.info('Deleted: channels:%d, shows:%d, movies:%d' %
                         (self.del_chn, self.del_shw, self.del_mov))
        self.logger.info('Total: channels:%d, shows:%d, movies:%d' %
                         (self.tot_chn, self.tot_shw, self.tot_mov))
        self.db.UpdateStatus(status,
                             int(time.time()) if status != 'ABORTED' else None,
                             None, 1 if full else 0, self.add_chn,
                             self.add_shw, self.add_mov, self.del_chn,
                             self.del_shw, self.del_mov, self.tot_chn,
                             self.tot_shw, self.tot_mov)

    def _init_record(self):
        self.index = 0
        self.film["title"] = ""
        self.film["aired"] = "1980-01-01 00:00:00"
        self.film["duration"] = "00:00:00"
        self.film["size"] = 0
        self.film["description"] = ""
        self.film["website"] = ""
        self.film["url_sub"] = ""
        self.film["url_video"] = ""
        self.film["url_video_sd"] = ""
        self.film["url_video_hd"] = ""
        self.film["airedepoch"] = 0
        self.film["geo"] = ""

    def _end_record(self, records):
        self.count = self.count + 1
        if self.count % self.db.flushBlockSize() == 0:
            #add 10% to record for final db update time in update_end
            percent = int(self.count * 100 / (records * 1.1))
            self.logger.info(
                'In progress (%d%%): channels:%d, shows:%d, movies:%d ...' %
                (percent, self.add_chn, self.add_shw, self.add_mov))
            self.notifier.UpdateUpdateProgress(
                percent if percent <= 100 else 100, self.count, self.add_chn,
                self.add_shw, self.add_mov)
            self.db.UpdateStatus(add_chn=self.add_chn,
                                 add_shw=self.add_shw,
                                 add_mov=self.add_mov,
                                 tot_chn=self.tot_chn + self.add_chn,
                                 tot_shw=self.tot_shw + self.add_shw,
                                 tot_mov=self.tot_mov + self.add_mov)
            (_, cnt_chn, cnt_shw,
             cnt_mov) = self.db.ftInsertFilm(self.film, True)
            self.db.ftFlushInsert()
        else:
            (_, cnt_chn, cnt_shw,
             cnt_mov) = self.db.ftInsertFilm(self.film, False)
        self.add_chn += cnt_chn
        self.add_shw += cnt_shw
        self.add_mov += cnt_mov

    def _add_value(self, val):
        if self.index == 0:
            if val != "":
                self.film["channel"] = val
        elif self.index == 1:
            if val != "":
                self.film["show"] = val[:255]
        elif self.index == 2:
            self.film["title"] = val[:255]
        elif self.index == 3:
            if len(val) == 10:
                self.film["aired"] = val[6:] + '-' + val[3:5] + '-' + val[:2]
        elif self.index == 4:
            if (self.film["aired"] != "1980-01-01 00:00:00") and (len(val)
                                                                  == 8):
                self.film["aired"] = self.film["aired"] + " " + val
        elif self.index == 5:
            if len(val) == 8:
                self.film["duration"] = val
        elif self.index == 6:
            if val != "":
                self.film["size"] = int(val)
        elif self.index == 7:
            self.film["description"] = val
        elif self.index == 8:
            self.film["url_video"] = val
        elif self.index == 9:
            self.film["website"] = val
        elif self.index == 10:
            self.film["url_sub"] = val
        elif self.index == 12:
            self.film["url_video_sd"] = self._make_url(val)
        elif self.index == 14:
            self.film["url_video_hd"] = self._make_url(val)
        elif self.index == 16:
            if val != "":
                self.film["airedepoch"] = int(val)
        elif self.index == 18:
            self.film["geo"] = val
        self.index = self.index + 1

    def _make_url(self, val):
        x = val.split('|')
        if len(x) == 2:
            cnt = int(x[0])
            return self.film["url_video"][:cnt] + x[1]
        else:
            return val

    def _decompress_bz2(self, sourcefile, destfile):
        blocksize = 8192
        try:
            with open(destfile, 'wb') as df, open(sourcefile, 'rb') as sf:
                decompressor = bz2.BZ2Decompressor()
                for data in iter(lambda: sf.read(blocksize), b''):
                    df.write(decompressor.decompress(data))
        except Exception as err:
            self.logger.error('bz2 decompression failed: {}'.format(err))
            return -1
        return 0

    def _decompress_gz(self, sourcefile, destfile):
        blocksize = 8192
        try:
            with open(destfile, 'wb') as df, gzip.open(sourcefile) as sf:
                for data in iter(lambda: sf.read(blocksize), b''):
                    df.write(data)
        except Exception as err:
            self.logger.error('gz decompression failed: {}'.format(err))
            return -1
        return 0
Exemple #3
0
class MediathekView(KodiPlugin):
    def __init__(self):
        super(MediathekView, self).__init__()
        self.settings = Settings()
        self.notifier = Notifier()
        self.db = Store(self.getNewLogger('Store'), self.notifier,
                        self.settings)

    def showMainMenu(self):
        # Search
        self.addFolderItem(30901, {'mode': "search"})
        # Search all
        self.addFolderItem(30902, {'mode': "searchall"})
        # Browse livestreams
        self.addFolderItem(30903, {'mode': "livestreams"})
        # Browse recently added
        self.addFolderItem(30904, {'mode': "recent", 'channel': 0})
        # Browse recently added by channel
        self.addFolderItem(30905, {'mode': "recentchannels"})
        # Browse by Initial->Show
        self.addFolderItem(30906, {'mode': "initial", 'channel': 0})
        # Browse by Channel->Initial->Shows
        self.addFolderItem(30907, {'mode': "channels"})
        # Database Information
        self.addActionItem(30908, {'mode': "action-dbinfo"})
        # Manual database update
        if self.settings.updmode == 1 or self.settings.updmode == 2:
            self.addActionItem(30909, {'mode': "action-dbupdate"})
        self.endOfDirectory()
        self._check_outdate()

    def showSearch(self, extendedsearch=False):
        settingid = 'lastsearch2' if extendedsearch is True else 'lastsearch1'
        headingid = 30902 if extendedsearch is True else 30901
        # are we returning from playback ?
        searchText = self.addon.getSetting(settingid)
        if len(searchText) > 0:
            # restore previous search
            self.db.Search(searchText, FilmUI(self), extendedsearch)
        else:
            # enter search term
            searchText = self.notifier.GetEnteredText('', headingid)
            if len(searchText) > 2:
                if self.db.Search(searchText, FilmUI(self),
                                  extendedsearch) > 0:
                    self.addon.setSetting(settingid, searchText)
            else:
                self.info(
                    'The following ERROR can be ignored. It is caused by the architecture of the Kodi Plugin Engine'
                )
                self.endOfDirectory(False, cacheToDisc=True)
                # self.showMainMenu()

    def showDbInfo(self):
        info = self.db.GetStatus()
        heading = self.language(30907)
        infostr = self.language({
            'NONE': 30941,
            'UNINIT': 30942,
            'IDLE': 30943,
            'UPDATING': 30944,
            'ABORTED': 30945
        }.get(info['status'], 30941))
        infostr = self.language(30965) % infostr
        totinfo = self.language(30971) % (info['tot_chn'], info['tot_shw'],
                                          info['tot_mov'])
        updatetype = self.language(30972 if info['fullupdate'] > 0 else 30973)
        if info['status'] == 'UPDATING' and info['filmupdate'] > 0:
            updinfo = self.language(30967) % (
                updatetype, datetime.datetime.fromtimestamp(
                    info['filmupdate']).strftime('%Y-%m-%d %H:%M:%S'),
                info['add_chn'], info['add_shw'], info['add_mov'])
        elif info['status'] == 'UPDATING':
            updinfo = self.language(30968) % (updatetype, info['add_chn'],
                                              info['add_shw'], info['add_mov'])
        elif info['lastupdate'] > 0 and info['filmupdate'] > 0:
            updinfo = self.language(30969) % (
                updatetype, datetime.datetime.fromtimestamp(
                    info['lastupdate']).strftime('%Y-%m-%d %H:%M:%S'),
                datetime.datetime.fromtimestamp(
                    info['filmupdate']).strftime('%Y-%m-%d %H:%M:%S'),
                info['add_chn'], info['add_shw'], info['add_mov'],
                info['del_chn'], info['del_shw'], info['del_mov'])
        elif info['lastupdate'] > 0:
            updinfo = self.language(30970) % (
                updatetype, datetime.datetime.fromtimestamp(
                    info['lastupdate']).strftime('%Y-%m-%d %H:%M:%S'),
                info['add_chn'], info['add_shw'], info['add_mov'],
                info['del_chn'], info['del_shw'], info['del_mov'])
        else:
            updinfo = self.language(30966)

        xbmcgui.Dialog().textviewer(
            heading, infostr + '\n\n' + totinfo + '\n\n' + updinfo)

    def doDownloadFilm(self, filmid, quality):
        if self.settings.downloadpath:
            film = self.db.RetrieveFilmInfo(filmid)
            if film is None:
                # film not found - should never happen
                return

            # check if the download path is reachable
            if not xbmcvfs.exists(self.settings.downloadpath):
                self.notifier.ShowError(self.language(30952),
                                        self.language(30979))
                return

            # get the best url
            if quality == '0' and film.url_video_sd:
                videourl = film.url_video_sd
            elif quality == '2' and film.url_video_hd:
                videourl = film.url_video_hd
            else:
                videourl = film.url_video

            # prepare names
            showname = mvutils.cleanup_filename(film.show)[:64]
            filestem = mvutils.cleanup_filename(film.title)[:64]
            extension = os.path.splitext(videourl)[1]
            if not extension:
                extension = u'.mp4'
            if not filestem:
                filestem = u'Film-{}'.format(film.id)
            if not showname:
                showname = filestem

            # prepare download directory and determine episode number
            dirname = self.settings.downloadpath + showname + '/'
            episode = 1
            if xbmcvfs.exists(dirname):
                (
                    _,
                    epfiles,
                ) = xbmcvfs.listdir(dirname)
                for epfile in epfiles:
                    match = re.search('^.* [eE][pP]([0-9]*)\.[^/]*$', epfile)
                    if match and len(match.groups()) > 0:
                        if episode <= int(match.group(1)):
                            episode = int(match.group(1)) + 1
            else:
                xbmcvfs.mkdir(dirname)

            # prepare resulting filenames
            fileepi = filestem + u' - EP%04d' % episode
            movname = dirname + fileepi + extension
            srtname = dirname + fileepi + u'.srt'
            ttmname = dirname + fileepi + u'.ttml'
            nfoname = dirname + fileepi + u'.nfo'

            # download video
            bgd = KodiBGDialog()
            bgd.Create(self.language(30974), fileepi + extension)
            try:
                bgd.Update(0)
                mvutils.url_retrieve_vfs(videourl, movname,
                                         bgd.UrlRetrieveHook)
                bgd.Close()
                self.notifier.ShowNotification(
                    30960,
                    self.language(30976).format(videourl))
            except Exception as err:
                bgd.Close()
                self.error('Failure downloading {}: {}', videourl, err)
                self.notifier.ShowError(
                    30952,
                    self.language(30975).format(videourl, err))

            # download subtitles
            if film.url_sub:
                bgd = KodiBGDialog()
                bgd.Create(30978, fileepi + u'.ttml')
                try:
                    bgd.Update(0)
                    mvutils.url_retrieve_vfs(film.url_sub, ttmname,
                                             bgd.UrlRetrieveHook)
                    try:
                        ttml2srt(xbmcvfs.File(ttmname, 'r'),
                                 xbmcvfs.File(srtname, 'w'))
                    except Exception as err:
                        self.info('Failed to convert to srt: {}', err)
                    bgd.Close()
                except Exception as err:
                    bgd.Close()
                    self.error('Failure downloading {}: {}', film.url_sub, err)

            # create NFO Files
            self._make_nfo_files(film, episode, dirname, nfoname, videourl)
        else:
            self.notifier.ShowError(30952, 30958)

    def doEnqueueFilm(self, filmid):
        self.info('Enqueue {}', filmid)

    def _check_outdate(self, maxage=172800):
        if self.settings.updmode != 1 and self.settings.updmode != 2:
            # no check with update disabled or update automatic
            return
        if self.db is None:
            # should never happen
            self.notifier.ShowOutdatedUnknown()
            return
        status = self.db.GetStatus()
        if status['status'] == 'NONE' or status['status'] == 'UNINIT':
            # should never happen
            self.notifier.ShowOutdatedUnknown()
            return
        elif status['status'] == 'UPDATING':
            # great... we are updating. nuthin to show
            return
        # lets check how old we are
        tsnow = int(time.time())
        tsold = int(status['lastupdate'])
        if tsnow - tsold > maxage:
            self.notifier.ShowOutdatedKnown(status)

    def _make_nfo_files(self, film, episode, dirname, filename, videourl):
        # create NFO files
        if not xbmcvfs.exists(dirname + 'tvshow.nfo'):
            try:
                with closing(xbmcvfs.File(dirname + 'tvshow.nfo',
                                          'w')) as file:
                    file.write(b'<tvshow>\n')
                    file.write(b'<id></id>\n')
                    file.write(
                        bytearray('\t<title>{}</title>\n'.format(film.show),
                                  'utf-8'))
                    file.write(
                        bytearray(
                            '\t<sorttitle>{}</sorttitle>\n'.format(film.show),
                            'utf-8'))
                    # TODO:				file.write( bytearray( '\t<year>{}</year>\n'.format( 2018 ), 'utf-8' ) )
                    file.write(
                        bytearray(
                            '\t<studio>{}</studio>\n'.format(film.channel),
                            'utf-8'))
                    file.write(b'</tvshow>\n')
            except Exception as err:
                self.error('Failure creating show NFO file for {}: {}',
                           videourl, err)

        try:
            with closing(xbmcvfs.File(filename, 'w')) as file:
                file.write(b'<episodedetails>\n')
                file.write(
                    bytearray('\t<title>{}</title>\n'.format(film.title),
                              'utf-8'))
                file.write(b'\t<season>1</season>\n')
                file.write(b'\t<autonumber>1</autonumber>\n')
                file.write(
                    bytearray('\t<episode>{}</episode>\n'.format(episode),
                              'utf-8'))
                file.write(
                    bytearray(
                        '\t<showtitle>{}</showtitle>\n'.format(film.show),
                        'utf-8'))
                file.write(
                    bytearray('\t<plot>{}</plot>\n'.format(film.description),
                              'utf-8'))
                file.write(
                    bytearray('\t<aired>{}</aired>\n'.format(film.aired),
                              'utf-8'))
                if film.seconds > 60:
                    file.write(
                        bytearray(
                            '\t<runtime>{}</runtime>\n'.format(
                                int(film.seconds / 60)), 'utf-8'))
                file.write(
                    bytearray('\t<studio>{}</studio\n'.format(film.channel),
                              'utf-8'))
                file.write(b'</episodedetails>\n')
        except Exception as err:
            self.error('Failure creating episode NFO file for {}: {}',
                       videourl, err)

    def Init(self):
        self.args = urlparse.parse_qs(sys.argv[2][1:])
        self.db.Init()
        if self.settings.HandleFirstRun():
            # TODO: Implement Issue #16
            pass

    def Do(self):
        # save last activity timestamp
        self.settings.ResetUserActivity()
        # process operation
        mode = self.args.get('mode', None)
        if mode is None:
            self.showMainMenu()
        elif mode[0] == 'search':
            self.showSearch()
        elif mode[0] == 'searchall':
            self.showSearch(extendedsearch=True)
        elif mode[0] == 'livestreams':
            self.db.GetLiveStreams(FilmUI(self,
                                          [xbmcplugin.SORT_METHOD_LABEL]))
        elif mode[0] == 'recent':
            channel = self.args.get('channel', [0])
            self.db.GetRecents(channel[0], FilmUI(self))
        elif mode[0] == 'recentchannels':
            self.db.GetRecentChannels(ChannelUI(self, nextdir='recent'))
        elif mode[0] == 'channels':
            self.db.GetChannels(ChannelUI(self, nextdir='shows'))
        elif mode[0] == 'action-dbinfo':
            self.showDbInfo()
        elif mode[0] == 'action-dbupdate':
            self.settings.TriggerUpdate()
            self.notifier.ShowNotification(30963, 30964, time=10000)
        elif mode[0] == 'initial':
            channel = self.args.get('channel', [0])
            self.db.GetInitials(channel[0], InitialUI(self))
        elif mode[0] == 'shows':
            channel = self.args.get('channel', [0])
            initial = self.args.get('initial', [None])
            self.db.GetShows(channel[0], initial[0], ShowUI(self))
        elif mode[0] == 'films':
            show = self.args.get('show', [0])
            self.db.GetFilms(show[0], FilmUI(self))
        elif mode[0] == 'download':
            filmid = self.args.get('id', [0])
            quality = self.args.get('quality', [1])
            self.doDownloadFilm(filmid[0], quality[0])
        elif mode[0] == 'enqueue':
            self.doEnqueueFilm(self.args.get('id', [0])[0])

        # cleanup saved searches
        if mode is None or mode[0] != 'search':
            self.addon.setSetting('lastsearch1', '')
        if mode is None or mode[0] != 'searchall':
            self.addon.setSetting('lastsearch2', '')

    def Exit(self):
        self.db.Exit()