From e239b81a185bfef61c248ba454460825bf402d30 Mon Sep 17 00:00:00 2001 From: Prinz23 Date: Sat, 11 Mar 2023 11:48:27 +0000 Subject: [PATCH] Change optionally add disk free space in response to three Web API endpoints. Change increase API version number to 15. Add actually use mount points to get disk free space. Add optional "freespace" parameter to endpoints: sg.getrootdirs, sg.addrootdir, sg.deleterootdir Change update help of affected endpoints. Fix explicitly save rootdirs after adding or deleting via Web API. --- CHANGES.md | 6 ++ gui/slick/interfaces/default/apiBuilder.tmpl | 24 +++++-- sickgear/helpers.py | 33 +++++---- sickgear/webapi.py | 75 +++++++++++++------- 4 files changed, 93 insertions(+), 45 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index de75eaeb..b367eae5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -9,6 +9,12 @@ * Change codebase cleanups * Change improve perf by using generators with `any` * Change deprecate processEpisode used by nzbToMedia to advise how to configure API instead +* Change optionally add disk free space in response to three Web API endpoints +* Change increase API version number to 15 +* Add actually use mount points to get disk free space +* Add optional "freespace" parameter to endpoints: sg.getrootdirs, sg.addrootdir, sg.deleterootdir +* Change update help of affected endpoints +* Fix explicitly save rootdirs after adding or deleting via Web API [develop changelog] diff --git a/gui/slick/interfaces/default/apiBuilder.tmpl b/gui/slick/interfaces/default/apiBuilder.tmpl index acf1fab1..4b696fb4 100644 --- a/gui/slick/interfaces/default/apiBuilder.tmpl +++ b/gui/slick/interfaces/default/apiBuilder.tmpl @@ -70,11 +70,11 @@ addList("Command", "Help", "?cmd=help", "sg.functions-list", "","", "default"); addOption("sg.functions-list", "$k", "&subject=$k", "", "", "#echo ('sb', 'sg')['sg' in $k]#") #end for addList("Command", "SickBeard.AddRootDir", "?cmd=sb.addrootdir", "sb.addrootdir"); -addList("Command", "SickGear.AddRootDir", "?cmd=sg.addrootdir", "sb.addrootdir"); +addList("Command", "SickGear.AddRootDir", "?cmd=sg.addrootdir", "sg.addrootdir"); addOption("Command", "SickBeard.CheckScheduler", "?cmd=sb.checkscheduler"); addOption("Command", "SickGear.CheckScheduler", "?cmd=sg.checkscheduler"); addList("Command", "SickBeard.DeleteRootDir", "?cmd=sb.deleterootdir", "sb.deleterootdir"); -addList("Command", "SickGear.DeleteRootDir", "?cmd=sg.deleterootdir", "sb.deleterootdir"); +addList("Command", "SickGear.DeleteRootDir", "?cmd=sg.deleterootdir", "sg.deleterootdir"); addOption("Command", "SickBeard.ForceSearch", "?cmd=sb.forcesearch"); addList("Command", "SickGear.ForceSearch", "?cmd=sg.forcesearch", "sg.forcesearch"); addOption("Command", "SickGear.SearchQueue", "?cmd=sg.searchqueue"); @@ -88,7 +88,7 @@ addList("Command", "SickGear.GetIndexers", "?cmd=sg.getindexers", "listindexers" addList("Command", "SickGear.GetIndexerIcon", "?cmd=sg.getindexericon", "getindexericon"); addList("Command", "SickGear.GetNetworkIcon", "?cmd=sg.getnetworkicon", "getnetworkicon"); addOption("Command", "SickBeard.GetRootDirs", "?cmd=sb.getrootdirs"); -addOption("Command", "SickGear.GetRootDirs", "?cmd=sg.getrootdirs"); +addList("Command", "SickGear.GetRootDirs", "?cmd=sg.getrootdirs", "sg.addfreespace"); addList("Command", "SickBeard.PauseBacklog", "?cmd=sb.pausebacklog", "sb.pausebacklog"); addList("Command", "SickGear.PauseBacklog", "?cmd=sg.pausebacklog", "sb.pausebacklog"); addOption("Command", "SickBeard.Ping", "?cmd=sb.ping"); @@ -621,10 +621,26 @@ addOption("sb.addrootdir-opt", "Optional Param", "", 1); addOption("sb.addrootdir-opt", "Default", "&default=1"); addOption("sb.addrootdir-opt", "Not Default", "&default=0"); -addOption("sb.deleterootdir", "C:\\Temp", "&location=C:\\Temp", "", 1); +addList("sg.addrootdir", "C:\\Temp", "&location=C:\\Temp", "sg.addrootdir-opt"); +addList("sg.addrootdir", "/usr/bin", "&location=/usr/bin/", "sg.addrootdir-opt"); +addList("sg.addrootdir", "S:\\Invalid_Location", "&location=S:\\Invalid_Location", "sg.addrootdir-opt"); + +addList("sg.addrootdir-opt", "Optional Param", "", "sg.addfreespace"); +addList("sg.addrootdir-opt", "Default", "&default=1", "sg.addfreespace"); +addList("sg.addrootdir-opt", "Not Default", "&default=0", "sg.addfreespace"); + +addOption("sb.deleterootdir", "C:\\Temp", "&location=C:\\Temp", 1); addOption("sb.deleterootdir", "/usr/bin", "&location=/usr/bin/"); addOption("sb.deleterootdir", "S:\\Invalid_Location", "&location=S:\\Invalid_Location"); +addList("sg.deleterootdir", "C:\\Temp", "&location=C:\\Temp", "sg.addfreespace"); +addList("sg.deleterootdir", "/usr/bin", "&location=/usr/bin/", "sg.addfreespace"); +addList("sg.deleterootdir", "S:\\Invalid_Location", "&location=S:\\Invalid_Location", "sg.addfreespace"); + +addOption("sg.addfreespace", "Optional Param", "", 1) +addOption("sg.addfreespace", "incl Freespace", "&freespace=1") +addOption("sg.addfreespace", "excl Freespace", "&freespace=0") + #for $cur_show_obj in $sortedShowList: addList("show.pause", "$cur_show_obj.name", "&indexerid=$cur_show_obj.prodid", "show.pause-opt"); #end for diff --git a/sickgear/helpers.py b/sickgear/helpers.py index 4c78e787..6d036afa 100644 --- a/sickgear/helpers.py +++ b/sickgear/helpers.py @@ -34,7 +34,7 @@ import sickgear from . import db, logger, notifiers from .common import cpu_presets, mediaExtensions, Overview, Quality, statusStrings, subtitleExtensions, \ ARCHIVED, DOWNLOADED, FAILED, IGNORED, SKIPPED, SNATCHED_ANY, SUBTITLED, UNAIRED, UNKNOWN, WANTED -from .sgdatetime import SGDatetime +from .sgdatetime import SGDatetime from lib.tvinfo_base.exceptions import * from exceptions_helper import ex, MultipleShowObjectsException @@ -1031,7 +1031,7 @@ def clear_cache(force=False): """ # clean out cache directory, remove everything > 12 hours old dirty = None - del_time = SGDatetime.timestamp_near(td=datetime.timedelta(hours=12)) + del_time = SGDatetime.timestamp_near(td=datetime.timedelta(hours=12)) direntry_args = dict(follow_symlinks=False) for direntry in scantree(sickgear.CACHE_DIR, ['images|rss|zoneinfo'], follow_symlinks=True): if direntry.is_file(**direntry_args) and (force or del_time > direntry.stat(**direntry_args).st_mtime): @@ -1342,7 +1342,7 @@ def delete_not_changed_in(paths, days=30, minutes=0): :param minutes: Purge files not modified in this number of minutes (default: 0 minutes) :return: tuple; number of files that qualify for deletion, number of qualifying files that failed to be deleted """ - del_time = SGDatetime.timestamp_near(td=datetime.timedelta(days=days, minutes=minutes)) + del_time = SGDatetime.timestamp_near(td=datetime.timedelta(days=days, minutes=minutes)) errors = 0 qualified = 0 for cur_path in (paths, [paths])[not isinstance(paths, list)]: @@ -1367,7 +1367,7 @@ def set_file_timestamp(filename, min_age=3, new_time=None): :param new_time: :type new_time: None or int """ - min_time = SGDatetime.timestamp_near(td=datetime.timedelta(days=min_age)) + min_time = SGDatetime.timestamp_near(td=datetime.timedelta(days=min_age)) try: if os.path.isfile(filename) and os.path.getmtime(filename) < min_time: os.utime(filename, new_time) @@ -1412,6 +1412,19 @@ def is_link(filepath): return os.path.islink(filepath) +def find_mount_point(path): + # type: (AnyStr) -> AnyStr + """ + returns the mount point for the given path + :param path: path to find the mount point + :return: mount point for path + """ + path = os.path.realpath(os.path.abspath(path)) + while not os.path.ismount(path): + path = os.path.dirname(path) + return path + + def df(): """ Return disk free space at known parent locations @@ -1424,17 +1437,9 @@ def df(): if sickgear.ROOT_DIRS and sickgear.DISPLAY_FREESPACE: targets = [] for path in sickgear.ROOT_DIRS.split('|')[1:]: - location_parts = os.path.splitdrive(path) - target = location_parts[0] - if 'win32' == sys.platform: - if not re.match('(?i)[a-z]:(?:\\\\)?$', target): - # simple drive letter not found, fallback to full path - target = path - min_output = False - elif sys.platform.startswith(('linux', 'darwin', 'sunos5')) or 'bsd' in sys.platform: - target = path - min_output = False + target = find_mount_point(path) if target and target not in targets: + min_output = False targets += [target] free = freespace(path) if None is not free: diff --git a/sickgear/webapi.py b/sickgear/webapi.py index f75c426a..48511e23 100644 --- a/sickgear/webapi.py +++ b/sickgear/webapi.py @@ -43,7 +43,7 @@ from . import classes, db, helpers, history, image_cache, logger, network_timezo from .common import ARCHIVED, DOWNLOADED, FAILED, IGNORED, SKIPPED, SNATCHED, SNATCHED_ANY, SNATCHED_BEST, \ SNATCHED_PROPER, UNAIRED, UNKNOWN, WANTED, Quality, qualityPresetStrings, statusStrings from .name_parser.parser import NameParser -from .helpers import starify +from .helpers import df, find_mount_point, starify from .indexers import indexer_api, indexer_config from .indexers.indexer_config import * from lib.tvinfo_base.exceptions import * @@ -150,7 +150,7 @@ else: class Api(webserve.BaseHandler): """ api class that returns json results """ - version = 14 # use an int since float-point is unpredictable + version = 15 # use an int since float-point is unpredictable def check_xsrf_cookie(self): pass @@ -801,38 +801,45 @@ def _getQualityMap(): return quality_map_inversed -def _getRootDirs(): - if "" == sickgear.ROOT_DIRS: - return {} +def _get_root_dirs(get_freespace=False): + # type: (bool) -> List[Dict] + """ + + :param get_freespace: include disk free space info in response + """ + dir_list = [] + if not sickgear.ROOT_DIRS: + return dir_list - rootDir = {} root_dirs = sickgear.ROOT_DIRS.split('|') - default_index = int(sickgear.ROOT_DIRS.split('|')[0]) - - rootDir["default_index"] = int(sickgear.ROOT_DIRS.split('|')[0]) - # remove default_index value from list (this fixes the offset) - root_dirs.pop(0) + default_index = int(root_dirs.pop(0)) if len(root_dirs) < default_index: - return {} + return dir_list # clean up the list - replace %xx escapes by their single-character equivalent root_dirs = [unquote_plus(x) for x in root_dirs] default_dir = root_dirs[default_index] - dir_list = [] - for root_dir in root_dirs: - valid = 1 + if root_dirs and get_freespace: + diskfree, _ = df() + + for cur_root_dir in root_dirs: try: - os.listdir(root_dir) + os.listdir(cur_root_dir) + valid = 1 except (BaseException, Exception): valid = 0 - default = 0 - if root_dir is default_dir: - default = 1 - dir_list.append({'valid': valid, 'location': root_dir, 'default': default}) + new_entry = {'valid': valid, 'location': cur_root_dir, 'default': int(cur_root_dir is default_dir)} + + if get_freespace: + # noinspection PyUnboundLocalVariable + new_entry.update({'free_space': next((space for disk, space in diskfree or [] + if disk == find_mount_point(cur_root_dir)), '')}) + + dir_list.append(new_entry) return dir_list @@ -1975,7 +1982,8 @@ class CMD_SickGearAddRootDir(ApiCall): _help = {"desc": "add a user configured parent directory", "requiredParameters": {"location": {"desc": "the full path to root (parent) directory"} }, - "optionalParameters": {"default": {"desc": "make the location passed the default root (parent) directory"} + "optionalParameters": {"default": {"desc": "make the location passed the default root (parent) directory"}, + "freespace": {"desc": "include free space of paths in response"} } } @@ -1984,6 +1992,7 @@ class CMD_SickGearAddRootDir(ApiCall): self.location, args = self.check_params(args, kwargs, "location", None, True, "string", []) # optional self.default, args = self.check_params(args, kwargs, "default", 0, False, "bool", []) + self.freespace, args = self.check_params(args, kwargs, "freespace", 0, False, "bool", []) # super, missing, help ApiCall.__init__(self, handler, args, kwargs) @@ -2026,7 +2035,9 @@ class CMD_SickGearAddRootDir(ApiCall): root_dirs_new = '|'.join([text_type(x) for x in root_dirs_new]) sickgear.ROOT_DIRS = root_dirs_new - return _responds(RESULT_SUCCESS, _getRootDirs(), msg="Root directories updated") + sickgear.save_config() + return _responds(RESULT_SUCCESS, _get_root_dirs(not self.sickbeard_call and self.freespace), + msg="Root directories updated") class CMD_SickBeardAddRootDir(CMD_SickGearAddRootDir): @@ -2084,20 +2095,24 @@ class CMD_SickBeardCheckScheduler(CMD_SickGearCheckScheduler): class CMD_SickGearDeleteRootDir(ApiCall): _help = {"desc": "delete a user configured parent directory", - "requiredParameters": {"location": {"desc": "the full path to root (parent) directory"}} + "requiredParameters": {"location": {"desc": "the full path to root (parent) directory"}}, + "optionalParameters": {"freespace": {"desc": "include free space of paths in response"} + } } def __init__(self, handler, args, kwargs): # required self.location, args = self.check_params(args, kwargs, "location", None, True, "string", []) # optional + self.freespace, args = self.check_params(args, kwargs, "freespace", 0, False, "bool", []) # super, missing, help ApiCall.__init__(self, handler, args, kwargs) def run(self): """ delete a user configured parent directory """ if sickgear.ROOT_DIRS == "": - return _responds(RESULT_FAILURE, _getRootDirs(), msg="No root directories detected") + return _responds(RESULT_FAILURE, _get_root_dirs(not self.sickbeard_call and self.freespace), + msg="No root directories detected") newIndex = 0 root_dirs_new = [] @@ -2124,8 +2139,10 @@ class CMD_SickGearDeleteRootDir(ApiCall): root_dirs_new = "|".join([text_type(x) for x in root_dirs_new]) sickgear.ROOT_DIRS = root_dirs_new + sickgear.save_config() # what if the root dir was not found? - return _responds(RESULT_SUCCESS, _getRootDirs(), msg="Root directory deleted") + return _responds(RESULT_SUCCESS, _get_root_dirs(not self.sickbeard_call and self.freespace), + msg="Root directory deleted") class CMD_SickBeardDeleteRootDir(CMD_SickGearDeleteRootDir): @@ -2374,18 +2391,22 @@ class CMD_SickGearGetqualityStrings(ApiCall): class CMD_SickGearGetRootDirs(ApiCall): - _help = {"desc": "get list of user configured parent directories"} + _help = {"desc": "get list of user configured parent directories", + "optionalParameters": {"freespace": {"desc": "include free space of paths in response"} + } + } def __init__(self, handler, args, kwargs): # required # optional + self.freespace, args = self.check_params(args, kwargs, "freespace", 0, False, "bool", []) # super, missing, help ApiCall.__init__(self, handler, args, kwargs) def run(self): """ get list of user configured parent directories """ - return _responds(RESULT_SUCCESS, _getRootDirs()) + return _responds(RESULT_SUCCESS, _get_root_dirs(not self.sickbeard_call and self.freespace)) class CMD_SickBeardGetRootDirs(CMD_SickGearGetRootDirs):