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.
This commit is contained in:
Prinz23 2023-03-11 11:48:27 +00:00 committed by JackDandy
parent 40badef803
commit e239b81a18
4 changed files with 93 additions and 45 deletions

View file

@ -9,6 +9,12 @@
* Change codebase cleanups * Change codebase cleanups
* Change improve perf by using generators with `any` * Change improve perf by using generators with `any`
* Change deprecate processEpisode used by nzbToMedia to advise how to configure API instead * 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] [develop changelog]

View file

@ -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]#") addOption("sg.functions-list", "$k", "&subject=$k", "", "", "#echo ('sb', 'sg')['sg' in $k]#")
#end for #end for
addList("Command", "SickBeard.AddRootDir", "?cmd=sb.addrootdir", "sb.addrootdir"); 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", "SickBeard.CheckScheduler", "?cmd=sb.checkscheduler");
addOption("Command", "SickGear.CheckScheduler", "?cmd=sg.checkscheduler"); addOption("Command", "SickGear.CheckScheduler", "?cmd=sg.checkscheduler");
addList("Command", "SickBeard.DeleteRootDir", "?cmd=sb.deleterootdir", "sb.deleterootdir"); 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"); addOption("Command", "SickBeard.ForceSearch", "?cmd=sb.forcesearch");
addList("Command", "SickGear.ForceSearch", "?cmd=sg.forcesearch", "sg.forcesearch"); addList("Command", "SickGear.ForceSearch", "?cmd=sg.forcesearch", "sg.forcesearch");
addOption("Command", "SickGear.SearchQueue", "?cmd=sg.searchqueue"); 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.GetIndexerIcon", "?cmd=sg.getindexericon", "getindexericon");
addList("Command", "SickGear.GetNetworkIcon", "?cmd=sg.getnetworkicon", "getnetworkicon"); addList("Command", "SickGear.GetNetworkIcon", "?cmd=sg.getnetworkicon", "getnetworkicon");
addOption("Command", "SickBeard.GetRootDirs", "?cmd=sb.getrootdirs"); 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", "SickBeard.PauseBacklog", "?cmd=sb.pausebacklog", "sb.pausebacklog");
addList("Command", "SickGear.PauseBacklog", "?cmd=sg.pausebacklog", "sb.pausebacklog"); addList("Command", "SickGear.PauseBacklog", "?cmd=sg.pausebacklog", "sb.pausebacklog");
addOption("Command", "SickBeard.Ping", "?cmd=sb.ping"); 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", "Default", "&default=1");
addOption("sb.addrootdir-opt", "Not Default", "&default=0"); 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", "/usr/bin", "&location=/usr/bin/");
addOption("sb.deleterootdir", "S:\\Invalid_Location", "&location=S:\\Invalid_Location"); 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: #for $cur_show_obj in $sortedShowList:
addList("show.pause", "$cur_show_obj.name", "&indexerid=$cur_show_obj.prodid", "show.pause-opt"); addList("show.pause", "$cur_show_obj.name", "&indexerid=$cur_show_obj.prodid", "show.pause-opt");
#end for #end for

View file

@ -34,7 +34,7 @@ import sickgear
from . import db, logger, notifiers from . import db, logger, notifiers
from .common import cpu_presets, mediaExtensions, Overview, Quality, statusStrings, subtitleExtensions, \ from .common import cpu_presets, mediaExtensions, Overview, Quality, statusStrings, subtitleExtensions, \
ARCHIVED, DOWNLOADED, FAILED, IGNORED, SKIPPED, SNATCHED_ANY, SUBTITLED, UNAIRED, UNKNOWN, WANTED 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 lib.tvinfo_base.exceptions import *
from exceptions_helper import ex, MultipleShowObjectsException from exceptions_helper import ex, MultipleShowObjectsException
@ -1031,7 +1031,7 @@ def clear_cache(force=False):
""" """
# clean out cache directory, remove everything > 12 hours old # clean out cache directory, remove everything > 12 hours old
dirty = None 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) direntry_args = dict(follow_symlinks=False)
for direntry in scantree(sickgear.CACHE_DIR, ['images|rss|zoneinfo'], follow_symlinks=True): 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): 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) :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 :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 errors = 0
qualified = 0 qualified = 0
for cur_path in (paths, [paths])[not isinstance(paths, list)]: 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: :param new_time:
:type new_time: None or int :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: try:
if os.path.isfile(filename) and os.path.getmtime(filename) < min_time: if os.path.isfile(filename) and os.path.getmtime(filename) < min_time:
os.utime(filename, new_time) os.utime(filename, new_time)
@ -1412,6 +1412,19 @@ def is_link(filepath):
return os.path.islink(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(): def df():
""" """
Return disk free space at known parent locations Return disk free space at known parent locations
@ -1424,17 +1437,9 @@ def df():
if sickgear.ROOT_DIRS and sickgear.DISPLAY_FREESPACE: if sickgear.ROOT_DIRS and sickgear.DISPLAY_FREESPACE:
targets = [] targets = []
for path in sickgear.ROOT_DIRS.split('|')[1:]: for path in sickgear.ROOT_DIRS.split('|')[1:]:
location_parts = os.path.splitdrive(path) target = find_mount_point(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
if target and target not in targets: if target and target not in targets:
min_output = False
targets += [target] targets += [target]
free = freespace(path) free = freespace(path)
if None is not free: if None is not free:

View file

@ -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, \ from .common import ARCHIVED, DOWNLOADED, FAILED, IGNORED, SKIPPED, SNATCHED, SNATCHED_ANY, SNATCHED_BEST, \
SNATCHED_PROPER, UNAIRED, UNKNOWN, WANTED, Quality, qualityPresetStrings, statusStrings SNATCHED_PROPER, UNAIRED, UNKNOWN, WANTED, Quality, qualityPresetStrings, statusStrings
from .name_parser.parser import NameParser 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 import indexer_api, indexer_config
from .indexers.indexer_config import * from .indexers.indexer_config import *
from lib.tvinfo_base.exceptions import * from lib.tvinfo_base.exceptions import *
@ -150,7 +150,7 @@ else:
class Api(webserve.BaseHandler): class Api(webserve.BaseHandler):
""" api class that returns json results """ """ 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): def check_xsrf_cookie(self):
pass pass
@ -801,38 +801,45 @@ def _getQualityMap():
return quality_map_inversed return quality_map_inversed
def _getRootDirs(): def _get_root_dirs(get_freespace=False):
if "" == sickgear.ROOT_DIRS: # type: (bool) -> List[Dict]
return {} """
: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('|') root_dirs = sickgear.ROOT_DIRS.split('|')
default_index = int(sickgear.ROOT_DIRS.split('|')[0]) default_index = int(root_dirs.pop(0))
rootDir["default_index"] = int(sickgear.ROOT_DIRS.split('|')[0])
# remove default_index value from list (this fixes the offset)
root_dirs.pop(0)
if len(root_dirs) < default_index: if len(root_dirs) < default_index:
return {} return dir_list
# clean up the list - replace %xx escapes by their single-character equivalent # clean up the list - replace %xx escapes by their single-character equivalent
root_dirs = [unquote_plus(x) for x in root_dirs] root_dirs = [unquote_plus(x) for x in root_dirs]
default_dir = root_dirs[default_index] default_dir = root_dirs[default_index]
dir_list = [] if root_dirs and get_freespace:
for root_dir in root_dirs: diskfree, _ = df()
valid = 1
for cur_root_dir in root_dirs:
try: try:
os.listdir(root_dir) os.listdir(cur_root_dir)
valid = 1
except (BaseException, Exception): except (BaseException, Exception):
valid = 0 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 return dir_list
@ -1975,7 +1982,8 @@ class CMD_SickGearAddRootDir(ApiCall):
_help = {"desc": "add a user configured parent directory", _help = {"desc": "add 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": {"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", []) self.location, args = self.check_params(args, kwargs, "location", None, True, "string", [])
# optional # optional
self.default, args = self.check_params(args, kwargs, "default", 0, False, "bool", []) 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 # super, missing, help
ApiCall.__init__(self, handler, args, kwargs) 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]) root_dirs_new = '|'.join([text_type(x) for x in root_dirs_new])
sickgear.ROOT_DIRS = 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): class CMD_SickBeardAddRootDir(CMD_SickGearAddRootDir):
@ -2084,20 +2095,24 @@ class CMD_SickBeardCheckScheduler(CMD_SickGearCheckScheduler):
class CMD_SickGearDeleteRootDir(ApiCall): class CMD_SickGearDeleteRootDir(ApiCall):
_help = {"desc": "delete a user configured parent directory", _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): def __init__(self, handler, args, kwargs):
# required # required
self.location, args = self.check_params(args, kwargs, "location", None, True, "string", []) self.location, args = self.check_params(args, kwargs, "location", None, True, "string", [])
# optional # optional
self.freespace, args = self.check_params(args, kwargs, "freespace", 0, False, "bool", [])
# super, missing, help # super, missing, help
ApiCall.__init__(self, handler, args, kwargs) ApiCall.__init__(self, handler, args, kwargs)
def run(self): def run(self):
""" delete a user configured parent directory """ """ delete a user configured parent directory """
if sickgear.ROOT_DIRS == "": 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 newIndex = 0
root_dirs_new = [] root_dirs_new = []
@ -2124,8 +2139,10 @@ class CMD_SickGearDeleteRootDir(ApiCall):
root_dirs_new = "|".join([text_type(x) for x in root_dirs_new]) root_dirs_new = "|".join([text_type(x) for x in root_dirs_new])
sickgear.ROOT_DIRS = root_dirs_new sickgear.ROOT_DIRS = root_dirs_new
sickgear.save_config()
# what if the root dir was not found? # 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): class CMD_SickBeardDeleteRootDir(CMD_SickGearDeleteRootDir):
@ -2374,18 +2391,22 @@ class CMD_SickGearGetqualityStrings(ApiCall):
class CMD_SickGearGetRootDirs(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): def __init__(self, handler, args, kwargs):
# required # required
# optional # optional
self.freespace, args = self.check_params(args, kwargs, "freespace", 0, False, "bool", [])
# super, missing, help # super, missing, help
ApiCall.__init__(self, handler, args, kwargs) ApiCall.__init__(self, handler, args, kwargs)
def run(self): def run(self):
""" get list of user configured parent directories """ """ 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): class CMD_SickBeardGetRootDirs(CMD_SickGearGetRootDirs):