diff --git a/data/interfaces/default/config_postProcessing.tmpl b/data/interfaces/default/config_postProcessing.tmpl index bc879148b608ff482a5663dcc25e2552a4ca933b..ae3ac4516f1ac3eea0d0f5047f17a160e4927d9f 100644 --- a/data/interfaces/default/config_postProcessing.tmpl +++ b/data/interfaces/default/config_postProcessing.tmpl @@ -135,6 +135,27 @@ </label> </div> + <label class="nocheck clearfix" for="process_method"> + <span class="component-title">Process Episode Method:</span> + <span class="component-desc"> + <select name="process_method" id="process_method" class="input-medium" > + #set $process_method_text = {'copy': "Copy", 'move': "Move"} + #for $curAction in ('copy', 'move'): + #if $sickbeard.PROCESS_METHOD == $curAction: + #set $process_method = "selected=\"selected\"" + #else + #set $process_method = "" + #end if + <option value="$curAction" $process_method>$process_method_text[$curAction]</option> + #end for + </select> + </span> + </label> + <label class="nocheck clearfix"> + <span class="component-title"> </span> + <span class="component-desc">What method should be used to put the renamed file in the TV directory?</span> + </label> + <div class="clearfix"></div> <input type="submit" class="btn config_submitter" value="Save Changes" /><br/> diff --git a/data/interfaces/default/inc_top.tmpl b/data/interfaces/default/inc_top.tmpl index b8e3c0e09491b9526f307d37f01613c5821caec8..9fc81fc1c8b21f531ff3c216f808b3f3e6b9bcc4 100644 --- a/data/interfaces/default/inc_top.tmpl +++ b/data/interfaces/default/inc_top.tmpl @@ -80,6 +80,7 @@ \$("#SubMenu a:contains('Clear History')").addClass('btn confirm').html('<span class="ui-icon ui-icon-trash pull-left"></span> Clear History </a>'); \$("#SubMenu a:contains('Trim History')").addClass('btn confirm').html('<span class="ui-icon ui-icon-trash pull-left"></span> Trim History </a>'); \$("#SubMenu a:contains('Trunc Episode Links')").addClass('btn confirm').html('<span class="ui-icon ui-icon-trash pull-left"></span> Trunc Episode Links </a>'); + \$("#SubMenu a:contains('Trunc Episode List Processed')").addClass('btn confirm').html('<span class="ui-icon ui-icon-trash pull-left"></span> Trunc Episode List Processed </a>'); \$("#SubMenu a[href='/errorlogs/clearerrors']").addClass('btn').html('<span class="ui-icon ui-icon-trash pull-left"></span> Clear Errors </a>'); \$("#SubMenu a:contains('Re-scan')").addClass('btn').html('<span class="ui-icon ui-icon-refresh pull-left"></span> Re-scan </a>'); \$("#SubMenu a:contains('Backlog Overview')").addClass('btn').html('<span class="ui-icon ui-icon-refresh pull-left"></span> Backlog Overview </a>'); diff --git a/sickbeard/__init__.py b/sickbeard/__init__.py index fffb1ee36f43c0aeaa88d1902f17a63b4e129784..3b42989bf90c8389ce1e26075c4e59e60709475f 100755 --- a/sickbeard/__init__.py +++ b/sickbeard/__init__.py @@ -182,6 +182,7 @@ RENAME_EPISODES = False PROCESS_AUTOMATICALLY = False PROCESS_AUTOMATICALLY_TORRENT = False KEEP_PROCESSED_DIR = False +PROCESS_METHOD = None MOVE_ASSOCIATED_FILES = False TV_DOWNLOAD_DIR = None TORRENT_DOWNLOAD_DIR = None @@ -437,7 +438,7 @@ def initialize(consoleLogging=True): USE_SYNOLOGYNOTIFIER, SYNOLOGYNOTIFIER_NOTIFY_ONSNATCH, SYNOLOGYNOTIFIER_NOTIFY_ONDOWNLOAD, SYNOLOGYNOTIFIER_NOTIFY_ONSUBTITLEDOWNLOAD, \ USE_MAIL, MAIL_USERNAME, MAIL_PASSWORD, MAIL_SERVER, MAIL_SSL, MAIL_FROM, MAIL_TO, MAIL_NOTIFY_ONSNATCH, \ NZBMATRIX_APIKEY, versionCheckScheduler, VERSION_NOTIFY, PROCESS_AUTOMATICALLY, PROCESS_AUTOMATICALLY_TORRENT, \ - KEEP_PROCESSED_DIR, TV_DOWNLOAD_DIR, TORRENT_DOWNLOAD_DIR, TVDB_BASE_URL, MIN_SEARCH_FREQUENCY, \ + KEEP_PROCESSED_DIR, PROCESS_METHOD, TV_DOWNLOAD_DIR, TORRENT_DOWNLOAD_DIR, TVDB_BASE_URL, MIN_SEARCH_FREQUENCY, \ showQueueScheduler, searchQueueScheduler, ROOT_DIRS, CACHE_DIR, ACTUAL_CACHE_DIR, TVDB_API_PARMS, \ NAMING_PATTERN, NAMING_MULTI_EP, NAMING_FORCE_FOLDERS, NAMING_ABD_PATTERN, NAMING_CUSTOM_ABD, \ RENAME_EPISODES, properFinderScheduler, PROVIDER_ORDER, autoPostProcesserScheduler, autoTorrentPostProcesserScheduler, \ @@ -525,8 +526,8 @@ def initialize(consoleLogging=True): TVDB_API_PARMS['cache'] = os.path.join(CACHE_DIR, 'tvdb') TVDB_BASE_URL = 'http://thetvdb.com/api/' + TVDB_API_KEY - - TOGGLE_SEARCH = check_setting_int(CFG, 'General', 'toggle_search', '') + + TOGGLE_SEARCH = check_setting_int(CFG, 'General', 'toggle_search', '') QUALITY_DEFAULT = check_setting_int(CFG, 'General', 'quality_default', SD) STATUS_DEFAULT = check_setting_int(CFG, 'General', 'status_default', SKIPPED) AUDIO_SHOW_DEFAULT = check_setting_str(CFG, 'General', 'audio_show_default', 'fr' ) @@ -570,6 +571,7 @@ def initialize(consoleLogging=True): PROCESS_AUTOMATICALLY_TORRENT = check_setting_int(CFG, 'General', 'process_automatically_torrent', 0) RENAME_EPISODES = check_setting_int(CFG, 'General', 'rename_episodes', 1) KEEP_PROCESSED_DIR = check_setting_int(CFG, 'General', 'keep_processed_dir', 1) + PROCESS_METHOD = check_setting_str(CFG, 'General', 'process_method', 'copy' if KEEP_PROCESSED_DIR else 'move') MOVE_ASSOCIATED_FILES = check_setting_int(CFG, 'General', 'move_associated_files', 0) CREATE_MISSING_SHOW_DIRS = check_setting_int(CFG, 'General', 'create_missing_show_dirs', 0) ADD_SHOWS_WO_DIR = check_setting_int(CFG, 'General', 'add_shows_wo_dir', 0) @@ -1298,6 +1300,7 @@ def save_config(): new_config['General']['tv_download_dir'] = TV_DOWNLOAD_DIR new_config['General']['torrent_download_dir'] = TORRENT_DOWNLOAD_DIR new_config['General']['keep_processed_dir'] = int(KEEP_PROCESSED_DIR) + new_config['General']['process_method'] = PROCESS_METHOD new_config['General']['move_associated_files'] = int(MOVE_ASSOCIATED_FILES) new_config['General']['process_automatically'] = int(PROCESS_AUTOMATICALLY) new_config['General']['process_automatically_torrent'] = int(PROCESS_AUTOMATICALLY_TORRENT) diff --git a/sickbeard/databases/mainDB.py b/sickbeard/databases/mainDB.py index 825d066b2687d6fecb362ddf1ac89bb6f0e21a70..ef67fcafb8fd5d811a51263b3e75808e672bbc6d 100644 --- a/sickbeard/databases/mainDB.py +++ b/sickbeard/databases/mainDB.py @@ -25,7 +25,7 @@ from sickbeard.providers.generic import GenericProvider from sickbeard import encodingKludge as ek from sickbeard.name_parser.parser import NameParser, InvalidNameException -MAX_DB_VERSION = 15 +MAX_DB_VERSION = 16 class MainSanityCheck(db.DBSanityCheck): @@ -103,7 +103,7 @@ class InitialSchema (db.SchemaUpgrade): "CREATE TABLE history (action NUMERIC, date NUMERIC, showid NUMERIC, season NUMERIC, episode NUMERIC, quality NUMERIC, resource TEXT, provider NUMERIC);", "CREATE TABLE episode_links (episode_id INTEGER, link TEXT);", "CREATE TABLE imdb_info (tvdb_id INTEGER PRIMARY KEY, imdb_id TEXT, title TEXT, year NUMERIC, akas TEXT, runtimes NUMERIC, genres TEXT, countries TEXT, country_codes TEXT, certificates TEXT, rating TEXT, votes INTEGER, last_update NUMERIC);" - + "CREATE TABLE processed_files (episode_id INTEGER, filename TEXT, md5 TEXT)" ] for query in queries: self.connection.action(query) @@ -717,6 +717,7 @@ class AddEpisodeLinkTable(AddSubtitlesSupport): if self.hasTable("episode_links") != True: self.connection.action("CREATE TABLE episode_links (episode_id INTEGER, link TEXT)") self.incDBVersion() + class AddIMDbInfo(AddEpisodeLinkTable): def test(self): return self.checkDBVersion() >= 15 @@ -724,4 +725,13 @@ class AddIMDbInfo(AddEpisodeLinkTable): def execute(self): if self.hasTable("imdb_info") != True: self.connection.action("CREATE TABLE imdb_info (tvdb_id INTEGER PRIMARY KEY, imdb_id TEXT, title TEXT, year NUMERIC, akas TEXT, runtimes NUMERIC, genres TEXT, countries TEXT, country_codes TEXT, certificates TEXT, rating TEXT, votes INTEGER, last_update NUMERIC)") - self.incDBVersion() \ No newline at end of file + self.incDBVersion() + +class AddProcessedFilesTable(AddIMDbInfo): + def test(self): + return self.checkDBVersion() >= 16 + + def execute(self): + if self.hasTable("processed_files") != True: + self.connection.action("CREATE TABLE processed_files (episode_id INTEGER, filename TEXT, md5 TEXT)") + self.incDBVersion() \ No newline at end of file diff --git a/sickbeard/helpers.py b/sickbeard/helpers.py index 93208c7eb6e291fb93833ead9629fc5094855e4b..0114f62918bd373be0524a5b4c28b20ab92ef84f 100644 --- a/sickbeard/helpers.py +++ b/sickbeard/helpers.py @@ -41,6 +41,7 @@ from sickbeard import db from sickbeard import encodingKludge as ek from sickbeard import notifiers +#from lib.linktastic import linktastic from lib.tvdb_api import tvdb_api, tvdb_exceptions import xml.etree.cElementTree as etree @@ -480,6 +481,25 @@ def moveFile(srcFile, destFile): copyFile(srcFile, destFile) ek.ek(os.unlink, srcFile) +# def hardlinkFile(srcFile, destFile): +# try: +# ek.ek(linktastic.link, srcFile, destFile) +# fixSetGroupID(destFile) +# except OSError: +# logger.log(u"Failed to create hardlink of " + srcFile + " at " + destFile + ". Copying instead", logger.ERROR) +# copyFile(srcFile, destFile) +# ek.ek(os.unlink, srcFile) +# +# def moveAndSymlinkFile(srcFile, destFile): +# try: +# ek.ek(os.rename, srcFile, destFile) +# fixSetGroupID(destFile) +# ek.ek(linktastic.symlink, destFile, srcFile) +# except OSError: +# logger.log(u"Failed to create symlink of " + srcFile + " at " + destFile + ". Copying instead", logger.ERROR) +# copyFile(srcFile, destFile) +# ek.ek(os.unlink, srcFile) + def del_empty_dirs(s_dir): b_empty = True diff --git a/sickbeard/postProcessor.py b/sickbeard/postProcessor.py index fb4b64ecef88a82cb6ebcfc3c252bbad2b10ac87..bb5155a6e96516a9f2f3acc7254d84e530ba0519 100755 --- a/sickbeard/postProcessor.py +++ b/sickbeard/postProcessor.py @@ -25,6 +25,7 @@ import shlex import subprocess import sickbeard +import hashlib from sickbeard import db from sickbeard import classes @@ -361,6 +362,44 @@ class PostProcessor(object): self._combined_file_operation(file_path, new_path, new_base_name, associated_files, action=_int_copy, subtitles=subtitles) + def _hardlink(self, file_path, new_path, new_base_name, associated_files=False): + """ + file_path: The full path of the media file to move + new_path: Destination path where we want to create a hard linked file + new_base_name: The base filename (no extension) to use during the link. Use None to keep the same name. + associated_files: Boolean, whether we should move similarly-named files too + """ + + def _int_hard_link(cur_file_path, new_file_path): + + self._log(u"Hard linking file from " + cur_file_path + " to " + new_file_path, logger.DEBUG) + try: + helpers.hardlinkFile(cur_file_path, new_file_path) + helpers.chmodAsParent(new_file_path) + except (IOError, OSError), e: + self._log("Unable to link file " + cur_file_path + " to " + new_file_path + ": "+ex(e), logger.ERROR) + raise e + self._combined_file_operation(file_path, new_path, new_base_name, associated_files, action=_int_hard_link) + + def _moveAndSymlink(self, file_path, new_path, new_base_name, associated_files=False): + """ + file_path: The full path of the media file to move + new_path: Destination path where we want to move the file to create a symbolic link to + new_base_name: The base filename (no extension) to use during the link. Use None to keep the same name. + associated_files: Boolean, whether we should move similarly-named files too + """ + + def _int_move_and_sym_link(cur_file_path, new_file_path): + + self._log(u"Moving then symbolic linking file from " + cur_file_path + " to " + new_file_path, logger.DEBUG) + try: + helpers.moveAndSymlinkFile(cur_file_path, new_file_path) + helpers.chmodAsParent(new_file_path) + except (IOError, OSError), e: + self._log("Unable to link file " + cur_file_path + " to " + new_file_path + ": " + ex(e), logger.ERROR) + raise e + self._combined_file_operation(file_path, new_path, new_base_name, associated_files, action=_int_move_and_sym_link) + def _history_lookup(self): """ Look up the NZB name in the history and see if it contains a record for self.nzb_name @@ -900,15 +939,49 @@ class PostProcessor(object): new_base_name = None new_file_name = self.file_name + with open(self.file_path, 'rb') as fh: + m = hashlib.md5() + while True: + data = fh.read(8192) + if not data: + break + m.update(data) + MD5 = m.hexdigest() + try: - # move the episode and associated files to the show dir - if sickbeard.KEEP_PROCESSED_DIR: - self._copy(self.file_path, dest_path, new_base_name, sickbeard.MOVE_ASSOCIATED_FILES) + + path,file=os.path.split(self.file_path) + + if sickbeard.TORRENT_DOWNLOAD_DIR == path: + #Action possible pour les torrent + if sickbeard.PROCESS_METHOD == "copy": + self._copy(self.file_path, dest_path, new_base_name, sickbeard.MOVE_ASSOCIATED_FILES) + elif sickbeard.PROCESS_METHOD == "move": + self._move(self.file_path, dest_path, new_base_name, sickbeard.MOVE_ASSOCIATED_FILES) + else: + logger.log(u"Unknown process method: " + str(sickbeard.PROCESS_METHOD), logger.ERROR) + raise exceptions.PostProcessingFailed("Unable to move the files to their new home") else: - self._move(self.file_path, dest_path, new_base_name, sickbeard.MOVE_ASSOCIATED_FILES) + #action pour le reste des fichier + if sickbeard.KEEP_PROCESSED_DIR: + self._copy(self.file_path, dest_path, new_base_name, sickbeard.MOVE_ASSOCIATED_FILES) + else: + self._move(self.file_path, dest_path, new_base_name, sickbeard.MOVE_ASSOCIATED_FILES) + except (OSError, IOError): raise exceptions.PostProcessingFailed("Unable to move the files to their new home") + myDB = db.DBConnection() + + ## INSERT MD5 of file + controlMD5 = {"episode_id" : int(ep_obj.tvdbid) } + NewValMD5 = {"filename" : new_base_name , + "md5" : MD5 + } + myDB.upsert("processed_files", NewValMD5, controlMD5) + + + # put the new location in the database for cur_ep in [ep_obj] + ep_obj.relatedEps: with cur_ep.lock: diff --git a/sickbeard/processTV.py b/sickbeard/processTV.py index 0d26c1b31d5363368f5b1d4512720068d8ce4be2..d879b4fdf9c305fd207e7744f52322ef13a58c60 100644 --- a/sickbeard/processTV.py +++ b/sickbeard/processTV.py @@ -20,6 +20,7 @@ from __future__ import with_statement import os import shutil +import hashlib import sickbeard from sickbeard import postProcessor @@ -103,39 +104,65 @@ def processDir (dirName, nzbName=None, recurse=False): cur_video_file_path = ek.ek(os.path.join, dirName, cur_video_file_path) - try: - processor = postProcessor.PostProcessor(cur_video_file_path, nzbName) - process_result = processor.process() - process_fail_message = "" - except exceptions.PostProcessingFailed, e: - process_result = False - process_fail_message = ex(e) - - returnStr += processor.log - - # as long as the postprocessing was successful delete the old folder unless the config wants us not to - if process_result: - - if len(videoFiles) == 1 and not sickbeard.KEEP_PROCESSED_DIR and \ - ek.ek(os.path.normpath, dirName) != ek.ek(os.path.normpath, sickbeard.TV_DOWNLOAD_DIR) and \ - ek.ek(os.path.normpath, dirName) != ek.ek(os.path.normpath, sickbeard.TORRENT_DOWNLOAD_DIR) and \ - len(remainingFolders) == 0: - - returnStr += logHelper(u"Deleting folder " + dirName, logger.DEBUG) - - try: - shutil.rmtree(dirName) - except (OSError, IOError), e: - returnStr += logHelper(u"Warning: unable to remove the folder " + dirName + ": " + ex(e), logger.WARNING) - - returnStr += logHelper(u"Processing succeeded for "+cur_video_file_path) - - else: - returnStr += logHelper(u"Processing failed for "+cur_video_file_path+": "+process_fail_message, logger.WARNING) - if sickbeard.TV_DOWNLOAD_DIR !="": - for i in range(1,5): - helpers.del_empty_dirs(sickbeard.TV_DOWNLOAD_DIR) - if sickbeard.TORRENT_DOWNLOAD_DIR !="": - for i in range(1,5): - helpers.del_empty_dirs(sickbeard.TORRENT_DOWNLOAD_DIR) + # IF VIDEO_FILE ALREADY PROCESS THEN CONTINUE + # TODO + + myDB = db.DBConnection() + + with open(cur_video_file_path, 'rb') as fh: + m = hashlib.md5() + while True: + data = fh.read(8192) + if not data: + break + m.update(data) + MD5 = m.hexdigest() + + logger.log("MD5 search : " + MD5, logger.DEBUG) + + sqlResults = myDB.select("select * from processed_files where md5 = \"" + MD5 + "\"") + + process_file = True + + for sqlProcess in sqlResults: + if sqlProcess["md5"] == MD5: + logger.log("File " + cur_video_file_path + " already processed for " + sqlProcess["filename"]) + process_file = False + + if process_file: + try: + processor = postProcessor.PostProcessor(cur_video_file_path, nzbName) + process_result = processor.process() + process_fail_message = "" + except exceptions.PostProcessingFailed, e: + process_result = False + process_fail_message = ex(e) + + returnStr += processor.log + + # as long as the postprocessing was successful delete the old folder unless the config wants us not to + if process_result: + + if len(videoFiles) == 1 \ + and ( ( not sickbeard.KEEP_PROCESSED_DIR and ek.ek(os.path.normpath, dirName) != ek.ek(os.path.normpath, sickbeard.TV_DOWNLOAD_DIR) ) \ + or ( sickbeard.PROCESS_METHOD == "move" and ek.ek(os.path.normpath, dirName) != ek.ek(os.path.normpath, sickbeard.TORRENT_DOWNLOAD_DIR) ) ) \ + and len(remainingFolders) == 0: + + returnStr += logHelper(u"Deleting folder " + dirName, logger.DEBUG) + + try: + shutil.rmtree(dirName) + except (OSError, IOError), e: + returnStr += logHelper(u"Warning: unable to remove the folder " + dirName + ": " + ex(e), logger.WARNING) + + returnStr += logHelper(u"Processing succeeded for "+cur_video_file_path) + + else: + returnStr += logHelper(u"Processing failed for "+cur_video_file_path+": "+process_fail_message, logger.WARNING) + if sickbeard.TV_DOWNLOAD_DIR !="": + for i in range(1,5): + helpers.del_empty_dirs(sickbeard.TV_DOWNLOAD_DIR) + if sickbeard.TORRENT_DOWNLOAD_DIR !="": + for i in range(1,5): + helpers.del_empty_dirs(sickbeard.TORRENT_DOWNLOAD_DIR) return returnStr diff --git a/sickbeard/tv.py b/sickbeard/tv.py index 32debdd08410c2e6c296359140341a0911a32b47..2cdb0ada54e6b549c06b1cc7198d8bb14cbceb3c 100644 --- a/sickbeard/tv.py +++ b/sickbeard/tv.py @@ -24,6 +24,7 @@ import threading import re import glob import traceback +import hashlib import sickbeard @@ -967,6 +968,7 @@ class TVShow(object): myDB.upsert("tv_shows", newValueDict, controlValueDict) + if self.imdbid: controlValueDict = {"tvdb_id": self.tvdbid} newValueDict = self.imdb_info @@ -1616,9 +1618,12 @@ class TVEpisode(object): "season": self.season, "episode": self.episode} + + # use a custom update/insert method to get the data into the DB myDB.upsert("tv_episodes", newValueDict, controlValueDict) + def fullPath (self): if self.location == None or self.location == "": return None diff --git a/sickbeard/webserve.py b/sickbeard/webserve.py index 906db3aadce5f895ffb2650a1d2b65fcd069b8b8..c041c3ce3092de15dd78a820121051496d59707b 100644 --- a/sickbeard/webserve.py +++ b/sickbeard/webserve.py @@ -768,6 +768,7 @@ class History: { 'title': 'Clear History', 'path': 'history/clearHistory' }, { 'title': 'Trim History', 'path': 'history/trimHistory' }, { 'title': 'Trunc Episode Links', 'path': 'history/truncEplinks' }, + { 'title': 'Trunc Episode List Processed', 'path': 'history/truncEpListProc' }, ] return _munge(t) @@ -801,6 +802,15 @@ class History: ui.notifications.message('All Episode Links Removed', messnum) redirect("/history") + @cherrypy.expose + def truncEpListProc(self): + myDB = db.DBConnection() + nbep=myDB.select("SELECT count(*) from processed_files") + myDB.action("DELETE FROM processed_files WHERE 1=1") + messnum = str(nbep[0][0]) + ' record for file processed delete' + ui.notifications.message('Clear list of file processed', messnum) + redirect("/history") + ConfigMenu = [ { 'title': 'General', 'path': 'config/general/' }, @@ -1086,7 +1096,7 @@ class ConfigPostProcessing: @cherrypy.expose def savePostProcessing(self, naming_pattern=None, naming_multi_ep=None, xbmc_data=None, xbmc__frodo__data=None, mediabrowser_data=None, synology_data=None, sony_ps3_data=None, wdtv_data=None, tivo_data=None, - use_banner=None, keep_processed_dir=None, process_automatically=None, process_automatically_torrent=None, rename_episodes=None, + use_banner=None, keep_processed_dir=None, process_method=None, process_automatically=None, process_automatically_torrent=None, rename_episodes=None, move_associated_files=None, tv_download_dir=None, torrent_download_dir=None, naming_custom_abd=None, naming_abd_pattern=None): results = [] @@ -1135,6 +1145,7 @@ class ConfigPostProcessing: sickbeard.PROCESS_AUTOMATICALLY = process_automatically sickbeard.PROCESS_AUTOMATICALLY_TORRENT = process_automatically_torrent sickbeard.KEEP_PROCESSED_DIR = keep_processed_dir + sickbeard.PROCESS_METHOD = process_method sickbeard.RENAME_EPISODES = rename_episodes sickbeard.MOVE_ASSOCIATED_FILES = move_associated_files sickbeard.NAMING_CUSTOM_ABD = naming_custom_abd @@ -3495,6 +3506,7 @@ class WebInterface: sickbeard.HOME_LAYOUT = layout redirect("/home") + @cherrypy.expose def setHomeSearch(self, search): @@ -3504,6 +3516,7 @@ class WebInterface: sickbeard.TOGGLE_SEARCH= search redirect("/home") + @cherrypy.expose def toggleDisplayShowSpecials(self, show): @@ -3741,4 +3754,4 @@ class WebInterface: errorlogs = ErrorLogs() - ui = UI() + ui = UI() \ No newline at end of file