Migrate more config pages to Starlette

Continues the migration to Starlette/Uvicorn by converting the
General, Folders, Switches, Special, and Categories config pages.

This commit:
- Refactors CherryPy class-based handlers into Starlette function-based routes.
- Updates form actions and methods in templates to align with the new routing.
- Standardizes API responses using `sabnzbd.api.report` for consistency.
- Updates Uvicorn and other related web dependencies.
This commit is contained in:
Safihre committed 2026-04-26 23:03:20 +02:00
1 parent 2f2136e24e
commit df875e37ae
9 files changed
+76 -72

No files matched your search

-1
View File
@@ -17,7 +17,6 @@
import sys
# Trick to show a better message on older Python
# releases that don't support walrus operator
if Python_39_is_required_to_run_SABnzbd := sys.hexversion < 0x03090000:
+4 -4
View File
@@ -9,12 +9,12 @@ setuptools==82.0.1
markdown==3.10.2
# Required on 32bit Windows, exclude it based on Python-version
importlib_metadata==8.5.0; python_version < '3.10'
importlib_resources==6.4.5; python_version < '3.10'
zipp==3.21.0; python_version < '3.10'
importlib_metadata==8.7.0; python_version < '3.10'
importlib_resources==6.5.2; python_version < '3.10'
zipp==3.23.0; python_version < '3.10'
# orjson does not support 32bit Windows, also exclude based on Python-version
orjson==3.10.12; python_version > '3.8'
orjson==3.11.3; python_version > '3.8'
# For the Windows build
pefile==2024.8.26; sys_platform == 'win32'
+2 -2
View File
@@ -10,7 +10,7 @@
<h5 class="darkred"><strong>$T('explain-relFolder'):</strong> <span class="path">$defdir</span></h5>
<!--#for $cur, $slot in enumerate($slotinfo)#-->
<!--#set $cansort = $slot.name != '*' and $slot.name != ''#-->
<form action="save" method="post" <!--#if $cansort#-->class="sorting-row"<!--#end if#-->>
<form action="categories/save" method="get" class="fullform" <!--#if $cansort#-->class="sorting-row"<!--#end if#-->>
<table class="catTable">
<!--#if $cur == 0#-->
<tr>
@@ -108,7 +108,7 @@
jQuery(document).ready(function() {
jQuery('.delCat').click(function() {
var theForm = jQuery(this).closest("form");
theForm.attr("action", "delete").submit();
theForm.attr("action", "categories/delete").submit();
});
// Add autocomplete and file-browser
@@ -8,7 +8,7 @@
<input type="checkbox" id="advanced-settings-button" name="advanced-settings-button"> $T('button-advanced')
</label>
</div>
<form action="folders/save" method="post" name="fullform" class="fullform" autocomplete="off">
<form action="folders/save" method="get" name="fullform" class="fullform" autocomplete="off">
<input type="hidden" id="apikey" name="apikey" value="$apikey" />
<input type="hidden" name="output" value="json" />
<input type="hidden" id="ajax" name="ajax" value="1" />
@@ -8,7 +8,7 @@
<input type="checkbox" id="advanced-settings-button" name="advanced-settings-button"> $T('button-advanced')
</label>
</div>
<form action="general/save" method="post" name="fullform" class="fullform" autocomplete="off">
<form action="general/save" method="get" name="fullform" class="fullform" autocomplete="off">
<input type="hidden" id="apikey" name="apikey" value="$apikey" />
<input type="hidden" id="ajax" name="ajax" value="1" />
<input type="hidden" name="output" value="json" />
@@ -3,7 +3,7 @@
<!--#include $webdir + "/_inc_header_uc.tmpl"#-->
<div class="colmask">
<form action="saveSpecial" method="post" autocomplete="off">
<form action="special/save" method="get" name="fullform" class="fullform" autocomplete="off">
<input type="hidden" id="apikey" name="apikey" value="$apikey" />
<div class="padTable">
<h4 class="darkred nomargin">$T('explain-special')</h4>
@@ -8,7 +8,7 @@
<input type="checkbox" id="advanced-settings-button" name="advanced-settings-button"> $T('button-advanced')
</label>
</div>
<form action="switches/save" method="post" name="fullform" class="fullform" autocomplete="off">
<form action="switches/save" method="get" name="fullform" class="fullform" autocomplete="off">
<input type="hidden" id="apikey" name="apikey" value="$apikey" />
<input type="hidden" id="ajax" name="ajax" value="1" />
<input type="hidden" name="output" value="json" />
+4 -4
View File
@@ -33,11 +33,11 @@ babelfish==0.6.1
rebulk==3.2.0
# New for starlette/uvicorn
starlette==0.47.3
anyio==4.10.0
starlette==0.48.0
anyio==4.11.0
sniffio==1.3.1
uvicorn==0.35.0
click==8.1.7
uvicorn==0.37.0
click==8.3.0
h11==0.16.0
python-multipart==0.0.20
httptools==0.6.4
+62 -57
View File
@@ -136,7 +136,6 @@ def secured_expose(
@functools.wraps(wrap_func)
async def internal_wrap(*args, **kwargs):
# All good, cool!
return await wrap_func(*args, **kwargs)
return internal_wrap
@@ -963,7 +962,7 @@ async def config_general_save(request: Request):
# Handle general options
for kw in GENERAL_LIST:
if msg := config.get_config("misc", kw).set(request.query_params.get(kw)):
return badParameterResponse(msg, ajax=request.query_params.get("ajax"))
return sabnzbd.api.report(request.query_params, error=msg)
# Handle special options
cfg.password.set(request.query_params.get("password"))
@@ -972,13 +971,7 @@ async def config_general_save(request: Request):
change_web_dir(web_dir)
config.save_config()
# Update CherryPy authentication
set_auth(cherrypy.config)
if request.query_params.get("ajax"):
return sabnzbd.api.report(data={"success": True, "restart_req": sabnzbd.RESTART_REQ})
else:
raise Raiser("/config")
return sabnzbd.api.report(request.query_params, data={"success": True, "restart_req": sabnzbd.RESTART_REQ})
@secured_expose(route="/config/general/uploadConfig", check_api_key=True, check_configlock=True)
@@ -1706,64 +1699,76 @@ class ConfigScheduling:
##############################################################################
class ConfigCats:
def __init__(self, root):
self.__root = root
# Page definitions - Config - Categories
##############################################################################
@secured_expose(check_configlock=True)
def index(request: Request):
conf = build_header(sabnzbd.WEB_DIR_CONFIG)
@secured_expose(route="/config/categories", check_configlock=True)
async def index_config_categories(request: Request):
conf = build_header(sabnzbd.WEB_DIR_CONFIG)
conf["scripts"] = list_scripts(default=True)
conf["defdir"] = cfg.complete_dir.get_clipped_path()
conf["scripts"] = list_scripts(default=True)
conf["defdir"] = cfg.complete_dir.get_clipped_path()
categories = config.get_ordered_categories()
new_cat_order = max(cat["order"] for cat in categories) + 1
categories = config.get_ordered_categories()
new_cat_order = max(cat["order"] for cat in categories) + 1
# Add empty line to add new categories
empty = {
"name": "",
"order": str(new_cat_order),
"pp": "-1",
"script": "",
"dir": "",
"newzbin": "",
"priority": DEFAULT_PRIORITY,
}
categories.insert(1, empty)
conf["slotinfo"] = categories
# Add empty line to add new categories
empty = {
"name": "",
"order": str(new_cat_order),
"pp": "-1",
"script": "",
"dir": "",
"newzbin": "",
"priority": DEFAULT_PRIORITY,
}
categories.insert(1, empty)
conf["slotinfo"] = categories
return template_filtered_response(
file=os.path.join(sabnzbd.WEB_DIR_CONFIG, "config_cat.tmpl"),
search_list=conf,
)
return template_filtered_response(
file=os.path.join(sabnzbd.WEB_DIR_CONFIG, "config_cat.tmpl"),
search_list=conf,
)
@secured_expose(check_api_key=True, check_configlock=True)
def delete(request: Request):
kwargs["section"] = "categories"
kwargs["keyword"] = request.query_params.get("name")
del_from_section(kwargs)
raise Raiser(self.__root)
@secured_expose(check_api_key=True, check_configlock=True)
def save(request: Request):
name = request.query_params.get("name", "*")
newname = request.query_params.get("newname", "")
if name == "*":
newname = name
@secured_expose(route="/config/categories/delete", check_api_key=True, check_configlock=True)
async def config_categories_delete(request: Request):
kw = {
"section": "categories",
"keyword": request.query_params.get("name"),
}
del_from_section(kw)
# Keep API response pattern consistent
return sabnzbd.api.report(request.query_params)
if newname:
# Check if this cat-dir is not sub-folder of incomplete
if same_directory(cfg.download_dir.get_path(), real_path(cfg.complete_dir.get_path(), kwargs["dir"])):
return T("Category folder cannot be a subfolder of the Temporary Download Folder.")
# Delete current one and replace with new one
if name:
config.delete("categories", name)
config.ConfigCat(newname.lower(), kwargs)
@secured_expose(route="/config/categories/save", check_api_key=True, check_configlock=True)
async def config_categories_save(request: Request):
name = request.query_params.get("name", "*")
newname = request.query_params.get("newname", "")
if name == "*":
newname = name
config.save_config()
raise Raiser(self.__root)
if newname:
# Build params dict for category configuration
cat_params = dict(request.query_params)
# Validate directory not under incomplete
if same_directory(
cfg.download_dir.get_path(),
real_path(cfg.complete_dir.get_path(), cat_params.get("dir", "")),
):
return sabnzbd.api.report(
request.query_params,
error=T("Category folder cannot be a subfolder of the Temporary Download Folder."),
)
# Delete current one and replace with new one
if name:
config.delete("categories", name)
config.ConfigCat(newname.lower(), cat_params)
config.save_config()
return sabnzbd.api.report(request.query_params)
##############################################################################