Browse Source

Transfer /status/ functions to API-functions

TODO: Update documentation and maybe some tests
pull/1923/head
Safihre 4 years ago
parent
commit
2aa0dbd93d
  1. 14
      interfaces/Glitter/templates/include_overlays.tmpl
  2. 55
      interfaces/Glitter/templates/static/javascripts/glitter.main.js
  3. 1
      interfaces/Glitter/templates/status.tmpl
  4. 1
      sabnzbd/__init__.py
  5. 130
      sabnzbd/api.py
  6. 435
      sabnzbd/interface.py
  7. 2
      tests/test_functional_misc.py

14
interfaces/Glitter/templates/include_overlays.tmpl

@ -116,7 +116,7 @@
<div class="col-sm-6">$T('dashboard-systemPerformance') &nbsp; </div> <div class="col-sm-6">$T('dashboard-systemPerformance') &nbsp; </div>
<div class="col-sm-6" data-bind="visible: hasPerformanceInfo"> <div class="col-sm-6" data-bind="visible: hasPerformanceInfo">
<span data-bind="text: statusInfo.pystone"></span> <span data-bind="text: statusInfo.pystone"></span>
<a href="#" data-bind="click: testDiskSpeed" data-tooltip="true" data-placement="right" title="$T('dashboard-repeatTest')"><span class="glyphicon glyphicon-repeat"></span></a> <a href="#" class="diskspeed-button" data-bind="click: loadStatusInfo" data-tooltip="true" data-placement="right" title="$T('dashboard-repeatTest')"><span class="glyphicon glyphicon-repeat"></span></a>
<small data-bind="truncatedText: statusInfo.cpumodel, length: 25, attr: { 'data-original-title': statusInfo.cpumodel }" data-tooltip="true"></small> <small data-bind="truncatedText: statusInfo.cpumodel, length: 25, attr: { 'data-original-title': statusInfo.cpumodel }" data-tooltip="true"></small>
</div> </div>
<div class="col-sm-6 col-loading" data-bind="visible: !hasPerformanceInfo()">$T('Glitter-loading')<span class="loader-dot-one">.</span><span class="loader-dot-two">.</span><span class="loader-dot-three">.</span></div> <div class="col-sm-6 col-loading" data-bind="visible: !hasPerformanceInfo()">$T('Glitter-loading')<span class="loader-dot-one">.</span><span class="loader-dot-two">.</span><span class="loader-dot-three">.</span></div>
@ -125,7 +125,7 @@
<div class="col-sm-6">$T('dashboard-downloadDirSpeed') &nbsp; </div> <div class="col-sm-6">$T('dashboard-downloadDirSpeed') &nbsp; </div>
<div class="col-sm-6" data-bind="visible: hasPerformanceInfo"> <div class="col-sm-6" data-bind="visible: hasPerformanceInfo">
<span data-bind="text: statusInfo.downloaddirspeed()"></span> MB/s <span data-bind="text: statusInfo.downloaddirspeed()"></span> MB/s
<a href="#" class="diskspeed-button" data-bind="click: testDiskSpeed" data-tooltip="true" data-placement="right" title="$T('dashboard-repeatTest')"><span class="glyphicon glyphicon-repeat"></span></a> <a href="#" class="diskspeed-button" data-bind="click: loadStatusInfo" data-tooltip="true" data-placement="right" title="$T('dashboard-repeatTest')"><span class="glyphicon glyphicon-repeat"></span></a>
<small>(<span data-bind="truncatedText: statusInfo.downloaddir, length: 24, attr: { 'data-original-title': statusInfo.downloaddir }" data-tooltip="true"></span>)</small> <small>(<span data-bind="truncatedText: statusInfo.downloaddir, length: 24, attr: { 'data-original-title': statusInfo.downloaddir }" data-tooltip="true"></span>)</small>
</div> </div>
<div class="col-sm-6 col-loading" data-bind="visible: !hasPerformanceInfo()">$T('Glitter-loading')<span class="loader-dot-one">.</span><span class="loader-dot-two">.</span><span class="loader-dot-three">.</span></div> <div class="col-sm-6 col-loading" data-bind="visible: !hasPerformanceInfo()">$T('Glitter-loading')<span class="loader-dot-one">.</span><span class="loader-dot-two">.</span><span class="loader-dot-three">.</span></div>
@ -134,7 +134,7 @@
<div class="col-sm-6">$T('dashboard-completeDirSpeed') &nbsp; </div> <div class="col-sm-6">$T('dashboard-completeDirSpeed') &nbsp; </div>
<div class="col-sm-6" data-bind="visible: hasPerformanceInfo"> <div class="col-sm-6" data-bind="visible: hasPerformanceInfo">
<span data-bind="text: statusInfo.completedirspeed()"></span> MB/s <span data-bind="text: statusInfo.completedirspeed()"></span> MB/s
<a href="#" class="diskspeed-button" data-bind="click: testDiskSpeed" data-tooltip="true" data-placement="right" title="$T('dashboard-repeatTest')"><span class="glyphicon glyphicon-repeat"></span></a> <a href="#" class="diskspeed-button" data-bind="click: loadStatusInfo" data-tooltip="true" data-placement="right" title="$T('dashboard-repeatTest')"><span class="glyphicon glyphicon-repeat"></span></a>
<small>(<span data-bind="truncatedText: statusInfo.completedir, length: 24, attr: { 'data-original-title': statusInfo.completedir }" data-tooltip="true"></span>)</small> <small>(<span data-bind="truncatedText: statusInfo.completedir, length: 24, attr: { 'data-original-title': statusInfo.completedir }" data-tooltip="true"></span>)</small>
</div> </div>
<div class="col-sm-6 col-loading" data-bind="visible: !hasPerformanceInfo()">$T('Glitter-loading')<span class="loader-dot-one">.</span><span class="loader-dot-two">.</span><span class="loader-dot-three">.</span></div> <div class="col-sm-6 col-loading" data-bind="visible: !hasPerformanceInfo()">$T('Glitter-loading')<span class="loader-dot-one">.</span><span class="loader-dot-two">.</span><span class="loader-dot-three">.</span></div>
@ -143,7 +143,7 @@
<div class="col-sm-6">$T('dashboard-internetBandwidth') &nbsp; </div> <div class="col-sm-6">$T('dashboard-internetBandwidth') &nbsp; </div>
<div class="col-sm-6" data-bind="visible: hasPerformanceInfo"> <div class="col-sm-6" data-bind="visible: hasPerformanceInfo">
<span data-bind="text: statusInfo.internetbandwidth()"></span> MB/s <span data-bind="text: statusInfo.internetbandwidth()"></span> MB/s
<a href="#" class="diskspeed-button" data-bind="click: testDiskSpeed" data-tooltip="true" data-placement="right" title="$T('dashboard-repeatTest')"><span class="glyphicon glyphicon-repeat"></span></a> <a href="#" class="diskspeed-button" data-bind="click: loadStatusInfo" data-tooltip="true" data-placement="right" title="$T('dashboard-repeatTest')"><span class="glyphicon glyphicon-repeat"></span></a>
<small>(<span data-bind="text: statusInfo.internetbandwidth()*8"></span> Mbps)</small> <small>(<span data-bind="text: statusInfo.internetbandwidth()*8"></span> Mbps)</small>
</div> </div>
<div class="col-sm-6 col-loading" data-bind="visible: !hasPerformanceInfo()">$T('Glitter-loading')<span class="loader-dot-one">.</span><span class="loader-dot-two">.</span><span class="loader-dot-three">.</span></div> <div class="col-sm-6 col-loading" data-bind="visible: !hasPerformanceInfo()">$T('Glitter-loading')<span class="loader-dot-one">.</span><span class="loader-dot-two">.</span><span class="loader-dot-three">.</span></div>
@ -170,7 +170,7 @@
</div> </div>
<div class="row options-function-box"> <div class="row options-function-box">
<div class="col-sm-6"> <div class="col-sm-6">
<a href="./status/showlog?apikey=$apikey" target="_blank" class="btn btn-default" data-tooltip="true" data-placement="top" title="$T('Glitter-logText')"> <a href="./api?mode=showlog&apikey=$apikey" target="_blank" class="btn btn-default" data-tooltip="true" data-placement="top" title="$T('Glitter-logText')">
<span class="glyphicon glyphicon-file"></span> $T('link-showLog') <span class="glyphicon glyphicon-file"></span> $T('link-showLog')
</a> </a>
</div> </div>
@ -285,8 +285,8 @@
<tr> <tr>
<td><span class="glyphicon glyphicon-folder-open"></span></td> <td><span class="glyphicon glyphicon-folder-open"></span></td>
<td class="row-wrap-text"><strong data-bind="html: \$data"></strong></td> <td class="row-wrap-text"><strong data-bind="html: \$data"></strong></td>
<td><a href="#" data-bind="click: \$root.folderProcess" data-action="add" class="hover-button" data-tooltip="true" data-placement="left" title="$T('Glitter-backToQueue')"><span class="glyphicon glyphicon-plus-sign"></span></a></td> <td><a href="#" data-bind="click: \$root.folderProcess" data-action="add_orphan" class="hover-button" data-tooltip="true" data-placement="left" title="$T('Glitter-backToQueue')"><span class="glyphicon glyphicon-plus-sign"></span></a></td>
<td><a href="#" data-bind="click: \$root.folderProcess" data-action="delete" class="hover-button" data-tooltip="true" data-placement="left" title="$T('Glitter-deleteJobAndFolders')"><span class="glyphicon glyphicon-trash"></span></a></td> <td><a href="#" data-bind="click: \$root.folderProcess" data-action="delete_orphan" class="hover-button" data-tooltip="true" data-placement="left" title="$T('Glitter-deleteJobAndFolders')"><span class="glyphicon glyphicon-trash"></span></a></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

55
interfaces/Glitter/templates/static/javascripts/glitter.main.js

@ -739,14 +739,26 @@ function ViewModel() {
// Full refresh? Only on click and for the status-screen // Full refresh? Only on click and for the status-screen
var statusFullRefresh = (event != undefined) && $('#options-status').hasClass('active'); var statusFullRefresh = (event != undefined) && $('#options-status').hasClass('active');
// Measure performance? Takes a while
var statusPerformance = (event != undefined) && $(event.currentTarget).hasClass('diskspeed-button');
// Make it spin if the user requested it otherwise we don't, // Make it spin if the user requested it otherwise we don't,
// because browsers use a lot of CPU for the animation // because browsers use a lot of CPU for the animation
if(statusFullRefresh) { if(statusFullRefresh) {
self.hasStatusInfo(false) self.hasStatusInfo(false)
} }
// Show loading text for performance measures
if(statusPerformance) {
self.hasPerformanceInfo(false)
}
// Load the custom status info // Load the custom status info
callAPI({ mode: 'fullstatus', skip_dashboard: (!statusFullRefresh)*1 }).then(function(data) { callAPI({
mode: 'status',
calculate_performance: statusPerformance*1,
skip_dashboard: (!statusFullRefresh)*1
}).then(function(data) {
// Update basic // Update basic
self.statusInfo.loglevel(data.status.loglevel) self.statusInfo.loglevel(data.status.loglevel)
self.statusInfo.folders(data.status.folders) self.statusInfo.folders(data.status.folders)
@ -774,8 +786,11 @@ function ViewModel() {
if(self.statusInfo.servers().length == 0) { if(self.statusInfo.servers().length == 0) {
self.statusInfo.loglevel.subscribe(function(newValue) { self.statusInfo.loglevel.subscribe(function(newValue) {
// Update log-level // Update log-level
callSpecialAPI('./status/change_loglevel/', { callAPI({
loglevel: newValue mode: "set_config",
section: "logging",
keyword: "log_level",
value: newValue
}); });
}) })
} }
@ -825,16 +840,6 @@ function ViewModel() {
}); });
} }
// Do a disk-speedtest
self.testDiskSpeed = function(item, event) {
self.hasPerformanceInfo(false)
// Run it and then display it
callSpecialAPI('./status/dashrefresh/').then(function() {
self.loadStatusInfo(true, true)
})
}
// Download a test-NZB // Download a test-NZB
self.testDownload = function(data, event) { self.testDownload = function(data, event) {
var nzbSize = $(event.target).data('size') var nzbSize = $(event.target).data('size')
@ -861,8 +866,10 @@ function ViewModel() {
// Unblock server // Unblock server
self.unblockServer = function(servername) { self.unblockServer = function(servername) {
callSpecialAPI("./status/unblock_server/", { callAPI({
server: servername mode: "status",
name: "unblock_server",
value: servername
}).then(function() { }).then(function() {
$("#modal-options").modal("hide"); $("#modal-options").modal("hide");
}) })
@ -920,7 +927,7 @@ function ViewModel() {
$('#options-orphans [data-tooltip="true"]').tooltip('hide') $('#options-orphans [data-tooltip="true"]').tooltip('hide')
// Show notification on delete // Show notification on delete
if($(htmlElement.currentTarget).data('action') == 'delete') { if($(htmlElement.currentTarget).data('action') == 'delete_orphan') {
showNotification('.main-notification-box-removing', 1000) showNotification('.main-notification-box-removing', 1000)
} else { } else {
// Adding back to queue // Adding back to queue
@ -928,8 +935,10 @@ function ViewModel() {
} }
// Activate // Activate
callSpecialAPI("./status/" + $(htmlElement.currentTarget).data('action'), { callAPI({
name: $("<div/>").html(folder).text() mode: "status",
name: $(htmlElement.currentTarget).data('action'),
value: $("<div/>").html(folder).text()
}).then(function() { }).then(function() {
// Refresh // Refresh
self.loadStatusInfo(true, true) self.loadStatusInfo(true, true)
@ -944,7 +953,10 @@ function ViewModel() {
// Show notification // Show notification
showNotification('.main-notification-box-removing-multiple', 0, self.statusInfo.folders().length) showNotification('.main-notification-box-removing-multiple', 0, self.statusInfo.folders().length)
// Delete them all // Delete them all
callSpecialAPI("./status/delete_all/").then(function() { callAPI({
mode: "status",
name: "delete_all_orphan"
}).then(function() {
// Remove notifcation and update screen // Remove notifcation and update screen
hideNotification() hideNotification()
self.loadStatusInfo(true, true) self.loadStatusInfo(true, true)
@ -958,7 +970,10 @@ function ViewModel() {
// Show notification // Show notification
showNotification('.main-notification-box-sendback') showNotification('.main-notification-box-sendback')
// Delete them all // Delete them all
callSpecialAPI("./status/add_all/").then(function() { callAPI({
mode: "status",
name: "add_all_orphan"
}).then(function() {
// Remove notifcation and update screen // Remove notifcation and update screen
hideNotification() hideNotification()
self.loadStatusInfo(true, true) self.loadStatusInfo(true, true)

1
interfaces/Glitter/templates/status.tmpl

@ -1 +0,0 @@
&nbsp;

1
sabnzbd/__init__.py

@ -164,7 +164,6 @@ LINUX_POWER = powersup.HAVE_DBUS
LOGFILE = None LOGFILE = None
WEBLOGFILE = None WEBLOGFILE = None
LOGHANDLER = None
GUIHANDLER = None GUIHANDLER = None
LOG_ALL = False LOG_ALL = False
AMBI_LOCALHOST = False AMBI_LOCALHOST = False

130
sabnzbd/api.py

@ -26,6 +26,7 @@ import gc
import datetime import datetime
import time import time
import json import json
import getpass
import cherrypy import cherrypy
from threading import Thread from threading import Thread
from typing import Tuple, Optional, List, Dict, Any from typing import Tuple, Optional, List, Dict, Any
@ -45,8 +46,10 @@ from sabnzbd.constants import (
import sabnzbd.config as config import sabnzbd.config as config
import sabnzbd.cfg as cfg import sabnzbd.cfg as cfg
from sabnzbd.skintext import SKIN_TEXT from sabnzbd.skintext import SKIN_TEXT
from sabnzbd.utils.diskspeed import diskspeedmeasure
from sabnzbd.utils.internetspeed import internetspeed
from sabnzbd.utils.pathbrowser import folders_at_path from sabnzbd.utils.pathbrowser import folders_at_path
from sabnzbd.utils.getperformance import getcpu from sabnzbd.utils.getperformance import getcpu, getpystone
from sabnzbd.misc import ( from sabnzbd.misc import (
loadavg, loadavg,
to_units, to_units,
@ -459,10 +462,62 @@ def _api_change_opts(name, kwargs):
def _api_fullstatus(name, kwargs): def _api_fullstatus(name, kwargs):
"""API: full history status""" """API: full history status"""
status = build_status(skip_dashboard=kwargs.get("skip_dashboard", 1)) status = build_status(
calculate_performance=kwargs.get("calculate_performance", 0), skip_dashboard=kwargs.get("skip_dashboard", 1)
)
return report(keyword="status", data=status) return report(keyword="status", data=status)
def _api_status(name, kwargs):
"""API: Dispatcher for mode=status, passing on the value"""
value = kwargs.get("value", "")
return _api_status_table.get(name, (_api_fullstatus, 2))[0](value, kwargs)
def _api_unblock_server(value, kwargs):
"""Unblock a blocked server"""
sabnzbd.Downloader.unblock(value)
return report()
def _api_delete_orphan(path, kwargs):
"""Remove orphaned job"""
if path:
path = os.path.join(cfg.download_dir.get_path(), path)
logging.info("Removing orphaned job %s", path)
remove_all(path, recursive=True)
return report()
else:
return report(_MSG_NO_ITEM)
def _api_delete_all_orphan(value, kwargs):
"""Remove all orphaned jobs"""
paths = sabnzbd.NzbQueue.scan_jobs(all_jobs=False, action=False)
for path in paths:
_api_delete_orphan(path, kwargs)
return report()
def _api_add_orphan(path, kwargs):
"""Add orphaned job"""
if path:
path = os.path.join(cfg.download_dir.get_path(), path)
logging.info("Re-adding orphaned job %s", path)
sabnzbd.NzbQueue.repair_job(path, None, None)
return report()
else:
return report(_MSG_NO_ITEM)
def _api_add_all_orphan(value, kwargs):
"""Add all orphaned jobs"""
paths = sabnzbd.NzbQueue.scan_jobs(all_jobs=False, action=False)
for path in paths:
_api_add_orphan(path, kwargs)
return report()
def _api_history(name, kwargs): def _api_history(name, kwargs):
"""API: accepts value(=nzo_id), start, limit, search, nzo_ids""" """API: accepts value(=nzo_id), start, limit, search, nzo_ids"""
value = kwargs.get("value", "") value = kwargs.get("value", "")
@ -615,6 +670,51 @@ def _api_warnings(name, kwargs):
return report(keyword="warnings", data=sabnzbd.GUIHANDLER.content()) return report(keyword="warnings", data=sabnzbd.GUIHANDLER.content())
LOG_API_RE = re.compile(rb"(apikey|api)([=:])[\w]+", re.I)
LOG_API_JSON_RE = re.compile(rb"'(apikey|api)': '[\w]+'", re.I)
LOG_USER_RE = re.compile(rb"(user|username)\s?=\s?[\S]+", re.I)
LOG_PASS_RE = re.compile(rb"(password)\s?=\s?[\S]+", re.I)
LOG_INI_HIDE_RE = re.compile(
rb"(email_pwd|email_account|email_to|rating_api_key|pushover_token|pushover_userkey|pushbullet_apikey|prowl_apikey|growl_password|growl_server|IPv[4|6] address)\s?=\s?[\S]+",
re.I,
)
LOG_HASH_RE = re.compile(rb"([a-fA-F\d]{25})", re.I)
def _api_showlog(name, kwargs):
"""Fetch the INI and the log-data and add a message at the top"""
log_data = b"--------------------------------\n\n"
log_data += b"The log includes a copy of your sabnzbd.ini with\nall usernames, passwords and API-keys removed."
log_data += b"\n\n--------------------------------\n"
with open(sabnzbd.LOGFILE, "rb") as f:
log_data += f.read()
with open(config.get_filename(), "rb") as f:
log_data += f.read()
# We need to remove all passwords/usernames/api-keys
log_data = LOG_API_RE.sub(b"apikey=<APIKEY>", log_data)
log_data = LOG_API_JSON_RE.sub(b"'apikey':<APIKEY>'", log_data)
log_data = LOG_USER_RE.sub(b"\\g<1>=<USER>", log_data)
log_data = LOG_PASS_RE.sub(b"password=<PASSWORD>", log_data)
log_data = LOG_INI_HIDE_RE.sub(b"\\1 = <REMOVED>", log_data)
log_data = LOG_HASH_RE.sub(b"<HASH>", log_data)
# Try to replace the username
try:
cur_user = getpass.getuser()
if cur_user:
log_data = log_data.replace(utob(cur_user), b"<USERNAME>")
except:
pass
# Set headers
cherrypy.response.headers["Content-Type"] = "application/x-download;charset=utf-8"
cherrypy.response.headers["Content-Disposition"] = 'attachment;filename="sabnzbd.log"'
return log_data
def _api_get_cats(name, kwargs): def _api_get_cats(name, kwargs):
return report(keyword="categories", data=list_cats(False)) return report(keyword="categories", data=list_cats(False))
@ -933,6 +1033,7 @@ _api_table = {
"change_script": (_api_change_script, 2), "change_script": (_api_change_script, 2),
"change_opts": (_api_change_opts, 2), "change_opts": (_api_change_opts, 2),
"fullstatus": (_api_fullstatus, 2), "fullstatus": (_api_fullstatus, 2),
"status": (_api_status, 2),
"history": (_api_history, 2), "history": (_api_history, 2),
"get_files": (_api_get_files, 2), "get_files": (_api_get_files, 2),
"move_nzf_bulk": (_api_move_nzf_bulk, 2), "move_nzf_bulk": (_api_move_nzf_bulk, 2),
@ -942,6 +1043,7 @@ _api_table = {
"resume": (_api_resume, 2), "resume": (_api_resume, 2),
"shutdown": (_api_shutdown, 3), "shutdown": (_api_shutdown, 3),
"warnings": (_api_warnings, 2), "warnings": (_api_warnings, 2),
"showlog": (_api_showlog, 3),
"config": (_api_config, 2), "config": (_api_config, 2),
"get_cats": (_api_get_cats, 2), "get_cats": (_api_get_cats, 2),
"get_scripts": (_api_get_scripts, 2), "get_scripts": (_api_get_scripts, 2),
@ -984,6 +1086,14 @@ _api_queue_table = {
"rating": (_api_queue_rating, 2), "rating": (_api_queue_rating, 2),
} }
_api_status_table = {
"unblock_server": (_api_unblock_server, 2),
"delete_orphan": (_api_delete_orphan, 2),
"delete_all_orphan": (_api_delete_all_orphan, 2),
"add_orphan": (_api_add_orphan, 2),
"add_all_orphan": (_api_add_all_orphan, 2),
}
_api_config_table = { _api_config_table = {
"speedlimit": (_api_config_speedlimit, 2), "speedlimit": (_api_config_speedlimit, 2),
"set_speedlimit": (_api_config_speedlimit, 2), "set_speedlimit": (_api_config_speedlimit, 2),
@ -1001,6 +1111,8 @@ def api_level(mode: str, name: str) -> int:
"""Return access level required for this API call""" """Return access level required for this API call"""
if mode == "queue" and name in _api_queue_table: if mode == "queue" and name in _api_queue_table:
return _api_queue_table[name][1] return _api_queue_table[name][1]
if mode == "status" and name in _api_status_table:
return _api_status_table[name][1]
if mode == "config" and name in _api_config_table: if mode == "config" and name in _api_config_table:
return _api_config_table[name][1] return _api_config_table[name][1]
if mode in _api_table: if mode in _api_table:
@ -1184,7 +1296,7 @@ def handle_cat_api(kwargs):
return name return name
def build_status(skip_dashboard: bool = False) -> Dict[str, Any]: def build_status(calculate_performance: bool = False, skip_dashboard: bool = False) -> Dict[str, Any]:
# build up header full of basic information # build up header full of basic information
info = build_header(trans_functions=False) info = build_header(trans_functions=False)
@ -1195,6 +1307,18 @@ def build_status(skip_dashboard: bool = False) -> Dict[str, Any]:
info["configfn"] = config.get_filename() info["configfn"] = config.get_filename()
info["warnings"] = sabnzbd.GUIHANDLER.content() info["warnings"] = sabnzbd.GUIHANDLER.content()
# Calculate performance measures, if requested
if int_conv(calculate_performance):
# PyStone
sabnzbd.PYSTONE_SCORE = getpystone()
# Diskspeed of download (aka incomplete) and complete directory:
sabnzbd.DOWNLOAD_DIR_SPEED = round(diskspeedmeasure(sabnzbd.cfg.download_dir.get_path()), 1)
sabnzbd.COMPLETE_DIR_SPEED = round(diskspeedmeasure(sabnzbd.cfg.complete_dir.get_path()), 1)
# Internet bandwidth
sabnzbd.INTERNET_BANDWIDTH = round(internetspeed(), 1)
# Dashboard: Speed of System # Dashboard: Speed of System
info["cpumodel"] = getcpu() info["cpumodel"] = getcpu()
info["pystone"] = sabnzbd.PYSTONE_SCORE info["pystone"] = sabnzbd.PYSTONE_SCORE

435
sabnzbd/interface.py

@ -54,10 +54,8 @@ from sabnzbd.misc import (
) )
from sabnzbd.filesystem import ( from sabnzbd.filesystem import (
real_path, real_path,
long_path,
globber, globber,
globber_full, globber_full,
remove_all,
clip_path, clip_path,
same_file, same_file,
setname_from_path, setname_from_path,
@ -68,9 +66,6 @@ import sabnzbd.cfg as cfg
import sabnzbd.notifier as notifier import sabnzbd.notifier as notifier
import sabnzbd.newsunpack import sabnzbd.newsunpack
from sabnzbd.utils.servertests import test_nntp_server_dict from sabnzbd.utils.servertests import test_nntp_server_dict
from sabnzbd.utils.diskspeed import diskspeedmeasure
from sabnzbd.utils.getperformance import getpystone
from sabnzbd.utils.internetspeed import internetspeed
import sabnzbd.utils.ssdp import sabnzbd.utils.ssdp
from sabnzbd.constants import DEF_STDCONFIG, DEFAULT_PRIORITY, CHEETAH_DIRECTIVES, EXCLUDED_GUESSIT_PROPERTIES from sabnzbd.constants import DEF_STDCONFIG, DEFAULT_PRIORITY, CHEETAH_DIRECTIVES, EXCLUDED_GUESSIT_PROPERTIES
from sabnzbd.lang import list_languages from sabnzbd.lang import list_languages
@ -194,7 +189,6 @@ def check_access(access_type: int = 4, warn_user: bool = False) -> bool:
if is_loopback_addr(remote_ip): if is_loopback_addr(remote_ip):
return True return True
is_allowed = False
if not cfg.local_ranges(): if not cfg.local_ranges():
# No local ranges defined, allow all private addresses by default # No local ranges defined, allow all private addresses by default
is_allowed = is_lan_addr(remote_ip) is_allowed = is_lan_addr(remote_ip)
@ -409,7 +403,6 @@ class MainPage:
# Add all sub-pages # Add all sub-pages
self.login = LoginPage() self.login = LoginPage()
self.status = Status("/status/")
self.config = ConfigPage("/config/") self.config = ConfigPage("/config/")
self.wizard = Wizard("/wizard/") self.wizard = Wizard("/wizard/")
@ -419,11 +412,6 @@ class MainPage:
if kwargs.get("skip_wizard") or config.get_servers(): if kwargs.get("skip_wizard") or config.get_servers():
info = build_header() info = build_header()
info["scripts"] = list_scripts(default=True)
info["script"] = "Default"
info["cat"] = "Default"
info["categories"] = list_cats(True)
info["have_rss_defined"] = bool(config.get_rss()) info["have_rss_defined"] = bool(config.get_rss())
info["have_watched_dir"] = bool(cfg.dirscan_dir()) info["have_watched_dir"] = bool(cfg.dirscan_dir())
@ -1429,10 +1417,10 @@ class ConfigRss:
uri = Strip(kwargs.get("uri")) uri = Strip(kwargs.get("uri"))
if feed and uri: if feed and uri:
try: try:
cfg = config.get_rss()[feed] rss_cfg = config.get_rss()[feed]
except KeyError: except KeyError:
cfg = None rss_cfg = None
if (not cfg) and uri: if not rss_cfg and uri:
kwargs["feed"] = feed kwargs["feed"] = feed
kwargs["uri"] = uri kwargs["uri"] = uri
config.ConfigRSS(feed, kwargs) config.ConfigRSS(feed, kwargs)
@ -1923,152 +1911,6 @@ class ConfigSorting:
raise Raiser(self.__root) raise Raiser(self.__root)
##############################################################################
LOG_API_RE = re.compile(rb"(apikey|api)(=|:)[\w]+", re.I)
LOG_API_JSON_RE = re.compile(rb"'(apikey|api)': '[\w]+'", re.I)
LOG_USER_RE = re.compile(rb"(user|username)\s?=\s?[\S]+", re.I)
LOG_PASS_RE = re.compile(rb"(password)\s?=\s?[\S]+", re.I)
LOG_INI_HIDE_RE = re.compile(
rb"(email_pwd|email_account|email_to|rating_api_key|pushover_token|pushover_userkey|pushbullet_apikey|prowl_apikey|growl_password|growl_server|IPv[4|6] address)\s?=\s?[\S]+",
re.I,
)
LOG_HASH_RE = re.compile(rb"([a-fA-F\d]{25})", re.I)
class Status:
def __init__(self, root):
self.__root = root
@secured_expose(check_configlock=True)
def index(self, **kwargs):
template = Template(file=os.path.join(sabnzbd.WEB_DIR, "status.tmpl"), compilerSettings=CHEETAH_DIRECTIVES)
return template.respond()
@secured_expose(check_api_key=True)
def showlog(self, **kwargs):
try:
sabnzbd.LOGHANDLER.flush()
except:
pass
# Fetch the INI and the log-data and add a message at the top
log_data = b"--------------------------------\n\n"
log_data += b"The log includes a copy of your sabnzbd.ini with\nall usernames, passwords and API-keys removed."
log_data += b"\n\n--------------------------------\n"
with open(sabnzbd.LOGFILE, "rb") as f:
log_data += f.read()
with open(config.get_filename(), "rb") as f:
log_data += f.read()
# We need to remove all passwords/usernames/api-keys
log_data = LOG_API_RE.sub(b"apikey=<APIKEY>", log_data)
log_data = LOG_API_JSON_RE.sub(b"'apikey':<APIKEY>'", log_data)
log_data = LOG_USER_RE.sub(b"\\g<1>=<USER>", log_data)
log_data = LOG_PASS_RE.sub(b"password=<PASSWORD>", log_data)
log_data = LOG_INI_HIDE_RE.sub(b"\\1 = <REMOVED>", log_data)
log_data = LOG_HASH_RE.sub(b"<HASH>", log_data)
# Try to replace the username
try:
import getpass
cur_user = getpass.getuser()
if cur_user:
log_data = log_data.replace(utob(cur_user), b"<USERNAME>")
except:
pass
# Set headers
cherrypy.response.headers["Content-Type"] = "application/x-download;charset=utf-8"
cherrypy.response.headers["Content-Disposition"] = 'attachment;filename="sabnzbd.log"'
return log_data
@secured_expose(check_api_key=True)
def change_loglevel(self, **kwargs):
cfg.log_level.set(kwargs.get("loglevel"))
config.save_config()
raise Raiser(self.__root)
@secured_expose(check_api_key=True)
def unblock_server(self, **kwargs):
sabnzbd.Downloader.unblock(kwargs.get("server"))
# Short sleep so that UI shows new server status
time.sleep(1.0)
raise Raiser(self.__root)
@secured_expose(check_api_key=True)
def delete(self, **kwargs):
orphan_delete(kwargs)
raise Raiser(self.__root)
@secured_expose(check_api_key=True)
def delete_all(self, **kwargs):
paths = sabnzbd.NzbQueue.scan_jobs(all_jobs=False, action=False)
for path in paths:
kwargs = {"name": path}
orphan_delete(kwargs)
raise Raiser(self.__root)
@secured_expose(check_api_key=True)
def add(self, **kwargs):
orphan_add(kwargs)
raise Raiser(self.__root)
@secured_expose(check_api_key=True)
def add_all(self, **kwargs):
paths = sabnzbd.NzbQueue.scan_jobs(all_jobs=False, action=False)
for path in paths:
kwargs = {"name": path}
orphan_add(kwargs)
raise Raiser(self.__root)
@secured_expose(check_api_key=True)
def dashrefresh(self, **kwargs):
# This function is run when Refresh button on Dashboard is clicked
# Put the time consuming dashboard functions here; they only get executed when the user clicks the Refresh button
# PyStone
sabnzbd.PYSTONE_SCORE = getpystone()
# Diskspeed of download (aka incomplete) directory:
dir_speed = diskspeedmeasure(sabnzbd.cfg.download_dir.get_path())
if dir_speed:
sabnzbd.DOWNLOAD_DIR_SPEED = round(dir_speed, 1)
else:
sabnzbd.DOWNLOAD_DIR_SPEED = 0
time.sleep(1.0)
# Diskspeed of complete directory:
dir_speed = diskspeedmeasure(sabnzbd.cfg.complete_dir.get_path())
if dir_speed:
sabnzbd.COMPLETE_DIR_SPEED = round(dir_speed, 1)
else:
sabnzbd.COMPLETE_DIR_SPEED = 0
# Internet bandwidth
sabnzbd.INTERNET_BANDWIDTH = round(internetspeed(), 1)
raise Raiser(self.__root) # Refresh screen
def orphan_delete(kwargs):
path = kwargs.get("name")
if path:
path = os.path.join(long_path(cfg.download_dir.get_path()), path)
logging.info("Removing orphaned job %s", path)
remove_all(path, recursive=True)
def orphan_add(kwargs):
path = kwargs.get("name")
if path:
path = os.path.join(long_path(cfg.download_dir.get_path()), path)
logging.info("Re-adding orphaned job %s", path)
sabnzbd.NzbQueue.repair_job(path, None, None)
def badParameterResponse(msg, ajax=None): def badParameterResponse(msg, ajax=None):
"""Return a html page with error message and a 'back' button""" """Return a html page with error message and a 'back' button"""
if ajax: if ajax:
@ -2187,141 +2029,142 @@ def GetRssLog(feed):
############################################################################## ##############################################################################
NOTIFY_OPTIONS = {} NOTIFY_OPTIONS = {
NOTIFY_OPTIONS["misc"] = ( "misc": (
"email_endjob", "email_endjob",
"email_cats", "email_cats",
"email_full", "email_full",
"email_server", "email_server",
"email_to", "email_to",
"email_from", "email_from",
"email_account", "email_account",
"email_pwd", "email_pwd",
"email_rss", "email_rss",
) ),
NOTIFY_OPTIONS["ncenter"] = ( "ncenter": (
"ncenter_enable", "ncenter_enable",
"ncenter_cats", "ncenter_cats",
"ncenter_prio_startup", "ncenter_prio_startup",
"ncenter_prio_download", "ncenter_prio_download",
"ncenter_prio_pause_resume", "ncenter_prio_pause_resume",
"ncenter_prio_pp", "ncenter_prio_pp",
"ncenter_prio_pp", "ncenter_prio_pp",
"ncenter_prio_complete", "ncenter_prio_complete",
"ncenter_prio_failed", "ncenter_prio_failed",
"ncenter_prio_disk_full", "ncenter_prio_disk_full",
"ncenter_prio_warning", "ncenter_prio_warning",
"ncenter_prio_error", "ncenter_prio_error",
"ncenter_prio_queue_done", "ncenter_prio_queue_done",
"ncenter_prio_other", "ncenter_prio_other",
"ncenter_prio_new_login", "ncenter_prio_new_login",
) ),
NOTIFY_OPTIONS["acenter"] = ( "acenter": (
"acenter_enable", "acenter_enable",
"acenter_cats", "acenter_cats",
"acenter_prio_startup", "acenter_prio_startup",
"acenter_prio_download", "acenter_prio_download",
"acenter_prio_pause_resume", "acenter_prio_pause_resume",
"acenter_prio_pp", "acenter_prio_pp",
"acenter_prio_complete", "acenter_prio_complete",
"acenter_prio_failed", "acenter_prio_failed",
"acenter_prio_disk_full", "acenter_prio_disk_full",
"acenter_prio_warning", "acenter_prio_warning",
"acenter_prio_error", "acenter_prio_error",
"acenter_prio_queue_done", "acenter_prio_queue_done",
"acenter_prio_other", "acenter_prio_other",
"acenter_prio_new_login", "acenter_prio_new_login",
) ),
NOTIFY_OPTIONS["ntfosd"] = ( "ntfosd": (
"ntfosd_enable", "ntfosd_enable",
"ntfosd_cats", "ntfosd_cats",
"ntfosd_prio_startup", "ntfosd_prio_startup",
"ntfosd_prio_download", "ntfosd_prio_download",
"ntfosd_prio_pause_resume", "ntfosd_prio_pause_resume",
"ntfosd_prio_pp", "ntfosd_prio_pp",
"ntfosd_prio_complete", "ntfosd_prio_complete",
"ntfosd_prio_failed", "ntfosd_prio_failed",
"ntfosd_prio_disk_full", "ntfosd_prio_disk_full",
"ntfosd_prio_warning", "ntfosd_prio_warning",
"ntfosd_prio_error", "ntfosd_prio_error",
"ntfosd_prio_queue_done", "ntfosd_prio_queue_done",
"ntfosd_prio_other", "ntfosd_prio_other",
"ntfosd_prio_new_login", "ntfosd_prio_new_login",
) ),
NOTIFY_OPTIONS["prowl"] = ( "prowl": (
"prowl_enable", "prowl_enable",
"prowl_cats", "prowl_cats",
"prowl_apikey", "prowl_apikey",
"prowl_prio_startup", "prowl_prio_startup",
"prowl_prio_download", "prowl_prio_download",
"prowl_prio_pause_resume", "prowl_prio_pause_resume",
"prowl_prio_pp", "prowl_prio_pp",
"prowl_prio_complete", "prowl_prio_complete",
"prowl_prio_failed", "prowl_prio_failed",
"prowl_prio_disk_full", "prowl_prio_disk_full",
"prowl_prio_warning", "prowl_prio_warning",
"prowl_prio_error", "prowl_prio_error",
"prowl_prio_queue_done", "prowl_prio_queue_done",
"prowl_prio_other", "prowl_prio_other",
"prowl_prio_new_login", "prowl_prio_new_login",
) ),
NOTIFY_OPTIONS["pushover"] = ( "pushover": (
"pushover_enable", "pushover_enable",
"pushover_cats", "pushover_cats",
"pushover_token", "pushover_token",
"pushover_userkey", "pushover_userkey",
"pushover_device", "pushover_device",
"pushover_prio_startup", "pushover_prio_startup",
"pushover_prio_download", "pushover_prio_download",
"pushover_prio_pause_resume", "pushover_prio_pause_resume",
"pushover_prio_pp", "pushover_prio_pp",
"pushover_prio_complete", "pushover_prio_complete",
"pushover_prio_failed", "pushover_prio_failed",
"pushover_prio_disk_full", "pushover_prio_disk_full",
"pushover_prio_warning", "pushover_prio_warning",
"pushover_prio_error", "pushover_prio_error",
"pushover_prio_queue_done", "pushover_prio_queue_done",
"pushover_prio_other", "pushover_prio_other",
"pushover_prio_new_login", "pushover_prio_new_login",
"pushover_emergency_retry", "pushover_emergency_retry",
"pushover_emergency_expire", "pushover_emergency_expire",
) ),
NOTIFY_OPTIONS["pushbullet"] = ( "pushbullet": (
"pushbullet_enable", "pushbullet_enable",
"pushbullet_cats", "pushbullet_cats",
"pushbullet_apikey", "pushbullet_apikey",
"pushbullet_device", "pushbullet_device",
"pushbullet_prio_startup", "pushbullet_prio_startup",
"pushbullet_prio_download", "pushbullet_prio_download",
"pushbullet_prio_pause_resume", "pushbullet_prio_pause_resume",
"pushbullet_prio_pp", "pushbullet_prio_pp",
"pushbullet_prio_complete", "pushbullet_prio_complete",
"pushbullet_prio_failed", "pushbullet_prio_failed",
"pushbullet_prio_disk_full", "pushbullet_prio_disk_full",
"pushbullet_prio_warning", "pushbullet_prio_warning",
"pushbullet_prio_error", "pushbullet_prio_error",
"pushbullet_prio_queue_done", "pushbullet_prio_queue_done",
"pushbullet_prio_other", "pushbullet_prio_other",
"pushbullet_prio_new_login", "pushbullet_prio_new_login",
) ),
NOTIFY_OPTIONS["nscript"] = ( "nscript": (
"nscript_enable", "nscript_enable",
"nscript_cats", "nscript_cats",
"nscript_script", "nscript_script",
"nscript_parameters", "nscript_parameters",
"nscript_prio_startup", "nscript_prio_startup",
"nscript_prio_download", "nscript_prio_download",
"nscript_prio_pause_resume", "nscript_prio_pause_resume",
"nscript_prio_pp", "nscript_prio_pp",
"nscript_prio_complete", "nscript_prio_complete",
"nscript_prio_failed", "nscript_prio_failed",
"nscript_prio_disk_full", "nscript_prio_disk_full",
"nscript_prio_warning", "nscript_prio_warning",
"nscript_prio_error", "nscript_prio_error",
"nscript_prio_queue_done", "nscript_prio_queue_done",
"nscript_prio_other", "nscript_prio_other",
"nscript_prio_new_login", "nscript_prio_new_login",
) ),
}
class ConfigNotify: class ConfigNotify:

2
tests/test_functional_misc.py

@ -32,7 +32,7 @@ class TestShowLogging(SABnzbdBaseTest):
def test_showlog(self): def test_showlog(self):
"""Test the output of the filtered-log button""" """Test the output of the filtered-log button"""
# Basic URL-fetching, easier than Selenium file download # Basic URL-fetching, easier than Selenium file download
log_result = get_url_result("status/showlog") log_result = get_api_result("showlog", extra_arguments={"output": "text"})
# Make sure it has basic log stuff # Make sure it has basic log stuff
assert "The log" in log_result assert "The log" in log_result

Loading…
Cancel
Save