1#!/usr/bin/python3 -OO 2# Copyright 2007-2021 The SABnzbd-Team <team@sabnzbd.org> 3# 4# This program is free software; you can redistribute it and/or 5# modify it under the terms of the GNU General Public License 6# as published by the Free Software Foundation; either version 2 7# of the License, or (at your option) any later version. 8# 9# This program is distributed in the hope that it will be useful, 10# but WITHOUT ANY WARRANTY; without even the implied warranty of 11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12# GNU General Public License for more details. 13# 14# You should have received a copy of the GNU General Public License 15# along with this program; if not, write to the Free Software 16# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 17 18""" 19sabnzbd.interface - webinterface 20""" 21 22import os 23import time 24from datetime import datetime 25import cherrypy 26import logging 27import urllib.request, urllib.parse, urllib.error 28import re 29import hashlib 30import socket 31import ssl 32import functools 33import ipaddress 34from threading import Thread 35from random import randint 36from xml.sax.saxutils import escape 37from Cheetah.Template import Template 38from typing import Optional, Callable, Union 39 40import sabnzbd 41import sabnzbd.rss 42from sabnzbd.misc import ( 43 to_units, 44 from_units, 45 time_format, 46 calc_age, 47 int_conv, 48 get_base_url, 49 is_ipv4_addr, 50 is_ipv6_addr, 51 opts_to_pp, 52 get_server_addrinfo, 53 is_lan_addr, 54 is_loopback_addr, 55 ip_in_subnet, 56 strip_ipv4_mapped_notation, 57) 58from sabnzbd.filesystem import real_path, long_path, globber, globber_full, remove_all, clip_path, same_file 59from sabnzbd.encoding import xml_name, utob 60import sabnzbd.config as config 61import sabnzbd.cfg as cfg 62import sabnzbd.notifier as notifier 63import sabnzbd.newsunpack 64from sabnzbd.utils.servertests import test_nntp_server_dict 65from sabnzbd.utils.diskspeed import diskspeedmeasure 66from sabnzbd.utils.getperformance import getpystone 67from sabnzbd.utils.internetspeed import internetspeed 68import sabnzbd.utils.ssdp 69from sabnzbd.constants import MEBI, DEF_SKIN_COLORS, DEF_STDCONFIG, DEF_MAIN_TMPL, DEFAULT_PRIORITY, CHEETAH_DIRECTIVES 70from sabnzbd.lang import list_languages 71from sabnzbd.api import ( 72 list_scripts, 73 list_cats, 74 del_from_section, 75 api_handler, 76 build_queue, 77 build_status, 78 retry_job, 79 build_header, 80 build_history, 81 del_hist_job, 82 Ttemplate, 83 build_queue_header, 84) 85 86############################################################################## 87# Security functions 88############################################################################## 89_MSG_ACCESS_DENIED = "Access denied" 90_MSG_ACCESS_DENIED_CONFIG_LOCK = "Access denied - Configuration locked" 91_MSG_ACCESS_DENIED_HOSTNAME = "Access denied - Hostname verification failed: https://sabnzbd.org/hostname-check" 92_MSG_MISSING_AUTH = "Missing authentication" 93_MSG_APIKEY_REQUIRED = "API Key Required" 94_MSG_APIKEY_INCORRECT = "API Key Incorrect" 95 96 97def secured_expose( 98 wrap_func: Optional[Callable] = None, 99 check_configlock: bool = False, 100 check_for_login: bool = True, 101 check_api_key: bool = False, 102 access_type: int = 4, 103) -> Union[Callable, str]: 104 """Wrapper for both cherrypy.expose and login/access check""" 105 if not wrap_func: 106 return functools.partial( 107 secured_expose, 108 check_configlock=check_configlock, 109 check_for_login=check_for_login, 110 check_api_key=check_api_key, 111 access_type=access_type, 112 ) 113 114 # Expose to cherrypy 115 wrap_func.exposed = True 116 117 @functools.wraps(wrap_func) 118 def internal_wrap(*args, **kwargs): 119 # Label for logging in this and other functions, handling X-Forwarded-For 120 # The cherrypy.request object allows adding custom attributes 121 if cherrypy.request.headers.get("X-Forwarded-For"): 122 cherrypy.request.remote_label = "%s (X-Forwarded-For: %s) [%s]" % ( 123 cherrypy.request.remote.ip, 124 cherrypy.request.headers.get("X-Forwarded-For"), 125 cherrypy.request.headers.get("User-Agent"), 126 ) 127 else: 128 cherrypy.request.remote_label = "%s [%s]" % ( 129 cherrypy.request.remote.ip, 130 cherrypy.request.headers.get("User-Agent"), 131 ) 132 133 # Log all requests 134 if cfg.api_logging(): 135 logging.debug( 136 "Request %s %s from %s %s", 137 cherrypy.request.method, 138 cherrypy.request.path_info, 139 cherrypy.request.remote_label, 140 kwargs, 141 ) 142 143 # Add X-Frame-Headers headers to page-requests 144 if cfg.x_frame_options(): 145 cherrypy.response.headers["X-Frame-Options"] = "SameOrigin" 146 147 # Check if config is locked 148 if check_configlock and cfg.configlock(): 149 cherrypy.response.status = 403 150 return _MSG_ACCESS_DENIED_CONFIG_LOCK 151 152 # Check if external access and if it's allowed 153 if not check_access(access_type=access_type, warn_user=True): 154 cherrypy.response.status = 403 155 return _MSG_ACCESS_DENIED 156 157 # Verify login status, only for non-key pages 158 if check_for_login and not check_api_key and not check_login(): 159 raise Raiser("/login/") 160 161 # Verify host used for the visit 162 if not check_hostname(): 163 cherrypy.response.status = 403 164 return _MSG_ACCESS_DENIED_HOSTNAME 165 166 # Some pages need correct API key 167 if check_api_key: 168 msg = check_apikey(kwargs) 169 if msg: 170 cherrypy.response.status = 403 171 return msg 172 173 # All good, cool! 174 return wrap_func(*args, **kwargs) 175 176 return internal_wrap 177 178 179def check_access(access_type: int = 4, warn_user: bool = False) -> bool: 180 """Check if external address is allowed given access_type: 181 1=nzb 182 2=api 183 3=full_api 184 4=webui 185 5=webui with login for external 186 """ 187 # Easy, it's allowed 188 if access_type <= cfg.inet_exposure(): 189 return True 190 191 remote_ip = cherrypy.request.remote.ip 192 193 # Check for localhost 194 if is_loopback_addr(remote_ip): 195 return True 196 197 is_allowed = False 198 if not cfg.local_ranges(): 199 # No local ranges defined, allow all private addresses by default 200 is_allowed = is_lan_addr(remote_ip) 201 else: 202 is_allowed = any(ip_in_subnet(remote_ip, r) for r in cfg.local_ranges()) 203 204 if not is_allowed and warn_user: 205 log_warning_and_ip(T("Refused connection from:")) 206 return is_allowed 207 208 209def check_hostname(): 210 """Check if hostname is allowed, to mitigate DNS-rebinding attack. 211 Similar to CVE-2019-5702, we need to add protection even 212 if only allowed to be accessed via localhost. 213 """ 214 # If login is enabled, no API-key can be deducted 215 if cfg.username() and cfg.password(): 216 return True 217 218 # Don't allow requests without Host 219 host = cherrypy.request.headers.get("Host") 220 if not host: 221 return False 222 223 # Remove the port-part (like ':8080'), if it is there, always on the right hand side. 224 # Not to be confused with IPv6 colons (within square brackets) 225 host = re.sub(":[0123456789]+$", "", host).lower() 226 227 # Fine if localhost or IP 228 if host == "localhost" or is_ipv4_addr(host) or is_ipv6_addr(host): 229 return True 230 231 # Check on the whitelist 232 if host in cfg.host_whitelist(): 233 return True 234 235 # Fine if ends with ".local" or ".local.", aka mDNS name 236 # See rfc6762 Multicast DNS 237 if host.endswith((".local", ".local.")): 238 return True 239 240 # Ohoh, bad 241 log_warning_and_ip(T('Refused connection with hostname "%s" from:') % host) 242 return False 243 244 245# Create a more unique ID for each instance 246COOKIE_SECRET = str(randint(1000, 100000) * os.getpid()) 247 248 249def set_login_cookie(remove=False, remember_me=False): 250 """We try to set a cookie as unique as possible 251 to the current user. Based on it's IP and the 252 current process ID of the SAB instance and a random 253 number, so cookies cannot be re-used 254 """ 255 salt = randint(1, 1000) 256 cookie_str = utob(str(salt) + cherrypy.request.remote.ip + COOKIE_SECRET) 257 cherrypy.response.cookie["login_cookie"] = hashlib.sha1(cookie_str).hexdigest() 258 cherrypy.response.cookie["login_cookie"]["path"] = "/" 259 cherrypy.response.cookie["login_cookie"]["httponly"] = 1 260 cherrypy.response.cookie["login_salt"] = salt 261 cherrypy.response.cookie["login_salt"]["path"] = "/" 262 cherrypy.response.cookie["login_salt"]["httponly"] = 1 263 264 # If we want to be remembered 265 if remember_me: 266 cherrypy.response.cookie["login_cookie"]["max-age"] = 3600 * 24 * 14 267 cherrypy.response.cookie["login_salt"]["max-age"] = 3600 * 24 * 14 268 269 # To remove 270 if remove: 271 cherrypy.response.cookie["login_cookie"]["expires"] = 0 272 cherrypy.response.cookie["login_salt"]["expires"] = 0 273 else: 274 # Notify about new login 275 notifier.send_notification(T("User logged in"), T("User logged in to the web interface"), "new_login") 276 277 278def check_login_cookie(): 279 # Do we have everything? 280 if "login_cookie" not in cherrypy.request.cookie or "login_salt" not in cherrypy.request.cookie: 281 return False 282 283 cookie_str = utob(str(cherrypy.request.cookie["login_salt"].value) + cherrypy.request.remote.ip + COOKIE_SECRET) 284 return cherrypy.request.cookie["login_cookie"].value == hashlib.sha1(cookie_str).hexdigest() 285 286 287def check_login(): 288 # Not when no authentication required or basic-auth is on 289 if not cfg.html_login() or not cfg.username() or not cfg.password(): 290 return True 291 292 # If we show login for external IP, by using access_type=6 we can check if IP match 293 if cfg.inet_exposure() == 5 and check_access(access_type=6): 294 return True 295 296 # Check the cookie 297 return check_login_cookie() 298 299 300def check_basic_auth(_, username, password): 301 """CherryPy basic authentication validation""" 302 return username == cfg.username() and password == cfg.password() 303 304 305def set_auth(conf): 306 """Set the authentication for CherryPy""" 307 if cfg.username() and cfg.password() and not cfg.html_login(): 308 conf.update( 309 { 310 "tools.auth_basic.on": True, 311 "tools.auth_basic.realm": "SABnzbd", 312 "tools.auth_basic.checkpassword": check_basic_auth, 313 } 314 ) 315 conf.update( 316 { 317 "/api": {"tools.auth_basic.on": False}, 318 "%s/api" % cfg.url_base(): {"tools.auth_basic.on": False}, 319 } 320 ) 321 else: 322 conf.update({"tools.auth_basic.on": False}) 323 324 325def check_apikey(kwargs): 326 """Check API-key or NZB-key 327 Return None when OK, otherwise an error message 328 """ 329 mode = kwargs.get("mode", "") 330 name = kwargs.get("name", "") 331 332 # Lookup required access level for the specific api-call 333 req_access = sabnzbd.api.api_level(mode, name) 334 if not check_access(req_access, warn_user=True): 335 return _MSG_ACCESS_DENIED 336 337 # Skip for auth and version calls 338 if mode in ("version", "auth"): 339 return None 340 341 # First check API-key, if OK that's sufficient 342 if not cfg.disable_key(): 343 key = kwargs.get("apikey") 344 if not key: 345 log_warning_and_ip( 346 T("API Key missing, please enter the api key from Config->General into your 3rd party program:") 347 ) 348 return _MSG_APIKEY_REQUIRED 349 elif req_access == 1 and key == cfg.nzb_key(): 350 return None 351 elif key == cfg.api_key(): 352 return None 353 else: 354 log_warning_and_ip(T("API Key incorrect, Use the api key from Config->General in your 3rd party program:")) 355 return _MSG_APIKEY_INCORRECT 356 357 # No active API-key, check web credentials instead 358 if cfg.username() and cfg.password(): 359 if check_login() or ( 360 kwargs.get("ma_username") == cfg.username() and kwargs.get("ma_password") == cfg.password() 361 ): 362 pass 363 else: 364 log_warning_and_ip( 365 T( 366 "Authentication missing, please enter username/password from Config->General into your 3rd party program:" 367 ) 368 ) 369 return _MSG_MISSING_AUTH 370 return None 371 372 373def log_warning_and_ip(txt): 374 """Include the IP and the Proxy-IP for warnings""" 375 if cfg.api_warnings(): 376 logging.warning("%s %s", txt, cherrypy.request.remote_label) 377 378 379############################################################################## 380# Helper raiser functions 381############################################################################## 382def Raiser(root: str = "", **kwargs): 383 # Add extras 384 if kwargs: 385 root = "%s?%s" % (root, urllib.parse.urlencode(kwargs)) 386 387 # Optionally add the leading /sabnzbd/ (or what the user set) 388 if not root.startswith(cfg.url_base()): 389 root = cherrypy.request.script_name + root 390 391 # Log the redirect 392 if cfg.api_logging(): 393 logging.debug("Request %s %s redirected to %s", cherrypy.request.method, cherrypy.request.path_info, root) 394 395 # Send the redirect 396 return cherrypy.HTTPRedirect(root) 397 398 399def queueRaiser(root, kwargs): 400 return Raiser(root, start=kwargs.get("start"), limit=kwargs.get("limit"), search=kwargs.get("search")) 401 402 403def rssRaiser(root, kwargs): 404 return Raiser(root, feed=kwargs.get("feed")) 405 406 407############################################################################## 408# Page definitions 409############################################################################## 410class MainPage: 411 def __init__(self): 412 self.__root = "/" 413 414 # Add all sub-pages 415 self.login = LoginPage() 416 self.queue = QueuePage("/queue/") 417 self.history = HistoryPage("/history/") 418 self.status = Status("/status/") 419 self.config = ConfigPage("/config/") 420 self.nzb = NzoPage("/nzb/") 421 self.wizard = Wizard("/wizard/") 422 423 @secured_expose 424 def index(self, **kwargs): 425 # Redirect to wizard if no servers are set 426 if kwargs.get("skip_wizard") or config.get_servers(): 427 info = build_header() 428 429 info["scripts"] = list_scripts(default=True) 430 info["script"] = "Default" 431 432 info["cat"] = "Default" 433 info["categories"] = list_cats(True) 434 info["have_rss_defined"] = bool(config.get_rss()) 435 info["have_watched_dir"] = bool(cfg.dirscan_dir()) 436 437 # Have logout only with HTML and if inet=5, only when we are external 438 info["have_logout"] = ( 439 cfg.username() 440 and cfg.password() 441 and ( 442 cfg.html_login() 443 and (cfg.inet_exposure() < 5 or (cfg.inet_exposure() == 5 and not check_access(access_type=6))) 444 ) 445 ) 446 447 bytespersec_list = sabnzbd.BPSMeter.get_bps_list() 448 info["bytespersec_list"] = ",".join([str(bps) for bps in bytespersec_list]) 449 450 template = Template( 451 file=os.path.join(sabnzbd.WEB_DIR, "main.tmpl"), searchList=[info], compilerSettings=CHEETAH_DIRECTIVES 452 ) 453 return template.respond() 454 else: 455 # Redirect to the setup wizard 456 raise cherrypy.HTTPRedirect("%s/wizard/" % cfg.url_base()) 457 458 @secured_expose(check_api_key=True) 459 def shutdown(self, **kwargs): 460 # Check for PID 461 pid_in = kwargs.get("pid") 462 if pid_in and int(pid_in) != os.getpid(): 463 return "Incorrect PID for this instance, remove PID from URL to initiate shutdown." 464 465 sabnzbd.shutdown_program() 466 return T("SABnzbd shutdown finished") 467 468 @secured_expose(check_api_key=True) 469 def pause(self, **kwargs): 470 sabnzbd.Scheduler.plan_resume(0) 471 sabnzbd.Downloader.pause() 472 raise Raiser(self.__root) 473 474 @secured_expose(check_api_key=True) 475 def resume(self, **kwargs): 476 sabnzbd.Scheduler.plan_resume(0) 477 sabnzbd.unpause_all() 478 raise Raiser(self.__root) 479 480 @secured_expose(check_api_key=True, access_type=1) 481 def api(self, **kwargs): 482 """Redirect to API-handler, we check the access_type in the API-handler""" 483 return api_handler(kwargs) 484 485 @secured_expose 486 def scriptlog(self, **kwargs): 487 """Needed for all skins, URL is fixed due to postproc""" 488 # No session key check, due to fixed URLs 489 name = kwargs.get("name") 490 if name: 491 history_db = sabnzbd.get_db_connection() 492 return ShowString(history_db.get_name(name), history_db.get_script_log(name)) 493 else: 494 raise Raiser(self.__root) 495 496 @secured_expose(check_api_key=True) 497 def retry(self, **kwargs): 498 """Duplicate of retry of History, needed for some skins""" 499 job = kwargs.get("job", "") 500 url = kwargs.get("url", "").strip() 501 pp = kwargs.get("pp") 502 cat = kwargs.get("cat") 503 script = kwargs.get("script") 504 if url: 505 sabnzbd.add_url(url, pp, script, cat, nzbname=kwargs.get("nzbname")) 506 del_hist_job(job, del_files=True) 507 raise Raiser(self.__root) 508 509 @secured_expose(check_api_key=True) 510 def retry_pp(self, **kwargs): 511 # Duplicate of History/retry_pp to please the SMPL skin :( 512 retry_job(kwargs.get("job"), kwargs.get("nzbfile"), kwargs.get("password")) 513 raise Raiser(self.__root) 514 515 @secured_expose 516 def robots_txt(self, **kwargs): 517 """Keep web crawlers out""" 518 cherrypy.response.headers["Content-Type"] = "text/plain" 519 return "User-agent: *\nDisallow: /\n" 520 521 @secured_expose 522 def description_xml(self, **kwargs): 523 """Provide the description.xml which was broadcast via SSDP""" 524 if is_lan_addr(cherrypy.request.remote.ip): 525 cherrypy.response.headers["Content-Type"] = "application/xml" 526 return utob(sabnzbd.utils.ssdp.server_ssdp_xml()) 527 else: 528 return None 529 530 531############################################################################## 532class Wizard: 533 def __init__(self, root): 534 self.__root = root 535 536 @secured_expose(check_configlock=True) 537 def index(self, **kwargs): 538 """Show the language selection page""" 539 if sabnzbd.WIN32: 540 from sabnzbd.utils.apireg import get_install_lng 541 542 cfg.language.set(get_install_lng()) 543 logging.debug('Installer language code "%s"', cfg.language()) 544 545 info = build_header(sabnzbd.WIZARD_DIR) 546 info["languages"] = list_languages() 547 template = Template( 548 file=os.path.join(sabnzbd.WIZARD_DIR, "index.html"), searchList=[info], compilerSettings=CHEETAH_DIRECTIVES 549 ) 550 return template.respond() 551 552 @secured_expose(check_configlock=True) 553 def one(self, **kwargs): 554 """Accept language and show server page""" 555 if kwargs.get("lang"): 556 cfg.language.set(kwargs.get("lang")) 557 558 # Always setup Glitter 559 change_web_dir("Glitter - Auto") 560 561 info = build_header(sabnzbd.WIZARD_DIR) 562 info["certificate_validation"] = sabnzbd.CERTIFICATE_VALIDATION 563 564 # Just in case, add server 565 servers = config.get_servers() 566 if not servers: 567 info["host"] = "" 568 info["port"] = "" 569 info["username"] = "" 570 info["password"] = "" 571 info["connections"] = "" 572 info["ssl"] = 0 573 info["ssl_verify"] = 2 574 else: 575 # Sort servers to get the first enabled one 576 server_names = sorted( 577 servers, 578 key=lambda svr: "%d%02d%s" 579 % (int(not servers[svr].enable()), servers[svr].priority(), servers[svr].displayname().lower()), 580 ) 581 for server in server_names: 582 # If there are multiple servers, just use the first enabled one 583 s = servers[server] 584 info["host"] = s.host() 585 info["port"] = s.port() 586 info["username"] = s.username() 587 info["password"] = s.password.get_stars() 588 info["connections"] = s.connections() 589 info["ssl"] = s.ssl() 590 info["ssl_verify"] = s.ssl_verify() 591 if s.enable(): 592 break 593 template = Template( 594 file=os.path.join(sabnzbd.WIZARD_DIR, "one.html"), searchList=[info], compilerSettings=CHEETAH_DIRECTIVES 595 ) 596 return template.respond() 597 598 @secured_expose(check_configlock=True) 599 def two(self, **kwargs): 600 """Accept server and show the final page for restart""" 601 # Save server details 602 if kwargs: 603 kwargs["enable"] = 1 604 handle_server(kwargs) 605 606 config.save_config() 607 608 # Show Restart screen 609 info = build_header(sabnzbd.WIZARD_DIR) 610 611 info["access_url"], info["urls"] = get_access_info() 612 info["download_dir"] = cfg.download_dir.get_clipped_path() 613 info["complete_dir"] = cfg.complete_dir.get_clipped_path() 614 615 template = Template( 616 file=os.path.join(sabnzbd.WIZARD_DIR, "two.html"), searchList=[info], compilerSettings=CHEETAH_DIRECTIVES 617 ) 618 return template.respond() 619 620 @secured_expose 621 def exit(self, **kwargs): 622 """Stop SABnzbd""" 623 sabnzbd.shutdown_program() 624 return T("SABnzbd shutdown finished") 625 626 627def get_access_info(): 628 """Build up a list of url's that sabnzbd can be accessed from""" 629 # Access_url is used to provide the user a link to SABnzbd depending on the host 630 cherryhost = cfg.cherryhost() 631 host = socket.gethostname().lower() 632 socks = [host] 633 634 if cherryhost == "0.0.0.0": 635 # Grab a list of all ips for the hostname 636 try: 637 addresses = socket.getaddrinfo(host, None) 638 except: 639 addresses = [] 640 for addr in addresses: 641 address = addr[4][0] 642 # Filter out ipv6 addresses (should not be allowed) 643 if ":" not in address and address not in socks: 644 socks.append(address) 645 socks.insert(0, "localhost") 646 elif cherryhost == "::": 647 # Grab a list of all ips for the hostname 648 addresses = socket.getaddrinfo(host, None) 649 for addr in addresses: 650 address = addr[4][0] 651 # Only ipv6 addresses will work 652 if ":" in address: 653 address = "[%s]" % address 654 if address not in socks: 655 socks.append(address) 656 socks.insert(0, "localhost") 657 elif cherryhost: 658 socks = [cherryhost] 659 660 # Add the current requested URL as the base 661 access_url = urllib.parse.urljoin(cherrypy.request.base, cfg.url_base()) 662 663 urls = [access_url] 664 for sock in socks: 665 if sock: 666 if cfg.enable_https() and cfg.https_port(): 667 url = "https://%s:%s%s" % (sock, cfg.https_port(), cfg.url_base()) 668 elif cfg.enable_https(): 669 url = "https://%s:%s%s" % (sock, cfg.cherryport(), cfg.url_base()) 670 else: 671 url = "http://%s:%s%s" % (sock, cfg.cherryport(), cfg.url_base()) 672 urls.append(url) 673 674 # Return a unique list 675 return access_url, set(urls) 676 677 678############################################################################## 679class LoginPage: 680 @secured_expose(check_for_login=False) 681 def index(self, **kwargs): 682 # Base output var 683 info = build_header(sabnzbd.WEB_DIR_CONFIG) 684 info["error"] = "" 685 686 # Logout? 687 if kwargs.get("logout"): 688 set_login_cookie(remove=True) 689 raise Raiser() 690 691 # Check if there's even a username/password set 692 if check_login(): 693 raise Raiser(cherrypy.request.script_name + "/") 694 695 # Check login info 696 if kwargs.get("username") == cfg.username() and kwargs.get("password") == cfg.password(): 697 # Save login cookie 698 set_login_cookie(remember_me=kwargs.get("remember_me", False)) 699 # Log the success 700 logging.info("Successful login from %s", cherrypy.request.remote_label) 701 # Redirect 702 raise Raiser(cherrypy.request.script_name + "/") 703 elif kwargs.get("username") or kwargs.get("password"): 704 info["error"] = T("Authentication failed, check username/password.") 705 # Warn about the potential security problem 706 logging.warning(T("Unsuccessful login attempt from %s"), cherrypy.request.remote_label) 707 708 # Show login 709 template = Template( 710 file=os.path.join(sabnzbd.WEB_DIR_CONFIG, "login", "main.tmpl"), 711 searchList=[info], 712 compilerSettings=CHEETAH_DIRECTIVES, 713 ) 714 return template.respond() 715 716 717############################################################################## 718class NzoPage: 719 def __init__(self, root): 720 self.__root = root 721 self.__cached_selection = {} # None 722 723 @secured_expose 724 def default(self, *args, **kwargs): 725 # Allowed URL's 726 # /nzb/SABnzbd_nzo_xxxxx/ 727 # /nzb/SABnzbd_nzo_xxxxx/details 728 # /nzb/SABnzbd_nzo_xxxxx/files 729 # /nzb/SABnzbd_nzo_xxxxx/bulk_operation 730 # /nzb/SABnzbd_nzo_xxxxx/save 731 nzo_id = None 732 for a in args: 733 if a.startswith("SABnzbd_nzo"): 734 nzo_id = a 735 break 736 737 nzo = sabnzbd.NzbQueue.get_nzo(nzo_id) 738 if nzo_id and nzo: 739 info, pnfo_list, bytespersec, q_size, bytes_left_previous_page = build_queue_header() 740 741 # /SABnzbd_nzo_xxxxx/bulk_operation 742 if "bulk_operation" in args: 743 return self.bulk_operation(nzo_id, kwargs) 744 745 # /SABnzbd_nzo_xxxxx/details 746 elif "details" in args: 747 info = self.nzo_details(info, pnfo_list, nzo_id) 748 749 # /SABnzbd_nzo_xxxxx/files 750 elif "files" in args: 751 info = self.nzo_files(info, nzo_id) 752 753 # /SABnzbd_nzo_xxxxx/save 754 elif "save" in args: 755 self.save_details(nzo_id, args, kwargs) 756 return # never reached 757 758 # /SABnzbd_nzo_xxxxx/ 759 else: 760 info = self.nzo_details(info, pnfo_list, nzo_id) 761 info = self.nzo_files(info, nzo_id) 762 763 template = Template( 764 file=os.path.join(sabnzbd.WEB_DIR, "nzo.tmpl"), searchList=[info], compilerSettings=CHEETAH_DIRECTIVES 765 ) 766 return template.respond() 767 else: 768 # Job no longer exists, go to main page 769 raise Raiser(urllib.parse.urljoin(self.__root, "../queue/")) 770 771 def nzo_details(self, info, pnfo_list, nzo_id): 772 slot = {} 773 n = 0 774 for pnfo in pnfo_list: 775 if pnfo.nzo_id == nzo_id: 776 nzo = sabnzbd.NzbQueue.get_nzo(nzo_id) 777 repair = pnfo.repair 778 unpack = pnfo.unpack 779 delete = pnfo.delete 780 unpackopts = opts_to_pp(repair, unpack, delete) 781 script = pnfo.script 782 if script is None: 783 script = "None" 784 cat = pnfo.category 785 if not cat: 786 cat = "None" 787 788 slot["nzo_id"] = str(nzo_id) 789 slot["cat"] = cat 790 slot["filename"] = nzo.final_name 791 slot["filename_clean"] = nzo.final_name 792 slot["password"] = nzo.password or "" 793 slot["script"] = script 794 slot["priority"] = str(pnfo.priority) 795 slot["unpackopts"] = str(unpackopts) 796 info["index"] = n 797 break 798 n += 1 799 800 info["slot"] = slot 801 info["scripts"] = list_scripts() 802 info["categories"] = list_cats() 803 info["noofslots"] = len(pnfo_list) 804 805 return info 806 807 def nzo_files(self, info, nzo_id): 808 active = [] 809 nzo = sabnzbd.NzbQueue.get_nzo(nzo_id) 810 if nzo: 811 pnfo = nzo.gather_info(full=True) 812 info["nzo_id"] = pnfo.nzo_id 813 info["filename"] = pnfo.filename 814 815 for nzf in pnfo.active_files: 816 checked = False 817 if nzf.nzf_id in self.__cached_selection and self.__cached_selection[nzf.nzf_id] == "on": 818 checked = True 819 active.append( 820 { 821 "filename": nzf.filename, 822 "mbleft": "%.2f" % (nzf.bytes_left / MEBI), 823 "mb": "%.2f" % (nzf.bytes / MEBI), 824 "size": to_units(nzf.bytes, "B"), 825 "sizeleft": to_units(nzf.bytes_left, "B"), 826 "nzf_id": nzf.nzf_id, 827 "age": calc_age(nzf.date), 828 "checked": checked, 829 } 830 ) 831 832 info["active_files"] = active 833 return info 834 835 def save_details(self, nzo_id, args, kwargs): 836 index = kwargs.get("index", None) 837 name = kwargs.get("name", None) 838 password = kwargs.get("password", None) 839 if password == "": 840 password = None 841 pp = kwargs.get("pp", None) 842 script = kwargs.get("script", None) 843 cat = kwargs.get("cat", None) 844 priority = kwargs.get("priority", None) 845 nzo = sabnzbd.NzbQueue.get_nzo(nzo_id) 846 847 if index is not None: 848 sabnzbd.NzbQueue.switch(nzo_id, index) 849 if name is not None: 850 sabnzbd.NzbQueue.change_name(nzo_id, name, password) 851 852 if cat is not None and nzo.cat is not cat and not (nzo.cat == "*" and cat == "Default"): 853 sabnzbd.NzbQueue.change_cat(nzo_id, cat, priority) 854 # Category changed, so make sure "Default" attributes aren't set again 855 if script == "Default": 856 script = None 857 if priority == "Default": 858 priority = None 859 if pp == "Default": 860 pp = None 861 862 if script is not None and nzo.script != script: 863 sabnzbd.NzbQueue.change_script(nzo_id, script) 864 if pp is not None and nzo.pp != pp: 865 sabnzbd.NzbQueue.change_opts(nzo_id, pp) 866 if priority is not None and nzo.priority != int(priority): 867 sabnzbd.NzbQueue.set_priority(nzo_id, priority) 868 869 raise Raiser(urllib.parse.urljoin(self.__root, "../queue/")) 870 871 def bulk_operation(self, nzo_id, kwargs): 872 self.__cached_selection = kwargs 873 if kwargs["action_key"] == "Delete": 874 for key in kwargs: 875 if kwargs[key] == "on": 876 sabnzbd.NzbQueue.remove_nzf(nzo_id, key, force_delete=True) 877 878 elif kwargs["action_key"] in ("Top", "Up", "Down", "Bottom"): 879 nzf_ids = [] 880 for key in kwargs: 881 if kwargs[key] == "on": 882 nzf_ids.append(key) 883 size = int_conv(kwargs.get("action_size", 1)) 884 if kwargs["action_key"] == "Top": 885 sabnzbd.NzbQueue.move_top_bulk(nzo_id, nzf_ids) 886 elif kwargs["action_key"] == "Up": 887 sabnzbd.NzbQueue.move_up_bulk(nzo_id, nzf_ids, size) 888 elif kwargs["action_key"] == "Down": 889 sabnzbd.NzbQueue.move_down_bulk(nzo_id, nzf_ids, size) 890 elif kwargs["action_key"] == "Bottom": 891 sabnzbd.NzbQueue.move_bottom_bulk(nzo_id, nzf_ids) 892 893 if sabnzbd.NzbQueue.get_nzo(nzo_id): 894 url = urllib.parse.urljoin(self.__root, nzo_id) 895 else: 896 url = urllib.parse.urljoin(self.__root, "../queue") 897 if url and not url.endswith("/"): 898 url += "/" 899 raise Raiser(url) 900 901 902############################################################################## 903class QueuePage: 904 def __init__(self, root): 905 self.__root = root 906 907 @secured_expose 908 def index(self, **kwargs): 909 start = int_conv(kwargs.get("start")) 910 limit = int_conv(kwargs.get("limit")) 911 search = kwargs.get("search") 912 info, _pnfo_list, _bytespersec = build_queue(start=start, limit=limit, trans=True, search=search) 913 914 template = Template( 915 file=os.path.join(sabnzbd.WEB_DIR, "queue.tmpl"), searchList=[info], compilerSettings=CHEETAH_DIRECTIVES 916 ) 917 return template.respond() 918 919 @secured_expose(check_api_key=True) 920 def delete(self, **kwargs): 921 uid = kwargs.get("uid") 922 del_files = int_conv(kwargs.get("del_files")) 923 if uid: 924 sabnzbd.NzbQueue.remove(uid, delete_all_data=del_files) 925 raise queueRaiser(self.__root, kwargs) 926 927 @secured_expose(check_api_key=True) 928 def purge(self, **kwargs): 929 sabnzbd.NzbQueue.remove_all(kwargs.get("search")) 930 raise queueRaiser(self.__root, kwargs) 931 932 @secured_expose(check_api_key=True) 933 def change_queue_complete_action(self, **kwargs): 934 """Action or script to be performed once the queue has been completed 935 Scripts are prefixed with 'script_' 936 """ 937 action = kwargs.get("action") 938 sabnzbd.change_queue_complete_action(action) 939 raise queueRaiser(self.__root, kwargs) 940 941 @secured_expose(check_api_key=True) 942 def switch(self, **kwargs): 943 uid1 = kwargs.get("uid1") 944 uid2 = kwargs.get("uid2") 945 if uid1 and uid2: 946 sabnzbd.NzbQueue.switch(uid1, uid2) 947 raise queueRaiser(self.__root, kwargs) 948 949 @secured_expose(check_api_key=True) 950 def change_opts(self, **kwargs): 951 nzo_id = kwargs.get("nzo_id") 952 pp = kwargs.get("pp", "") 953 if nzo_id and pp and pp.isdigit(): 954 sabnzbd.NzbQueue.change_opts(nzo_id, int(pp)) 955 raise queueRaiser(self.__root, kwargs) 956 957 @secured_expose(check_api_key=True) 958 def change_script(self, **kwargs): 959 nzo_id = kwargs.get("nzo_id") 960 script = kwargs.get("script", "") 961 if nzo_id and script: 962 if script == "None": 963 script = None 964 sabnzbd.NzbQueue.change_script(nzo_id, script) 965 raise queueRaiser(self.__root, kwargs) 966 967 @secured_expose(check_api_key=True) 968 def change_cat(self, **kwargs): 969 nzo_id = kwargs.get("nzo_id") 970 cat = kwargs.get("cat", "") 971 if nzo_id and cat: 972 if cat == "None": 973 cat = None 974 sabnzbd.NzbQueue.change_cat(nzo_id, cat) 975 976 raise queueRaiser(self.__root, kwargs) 977 978 @secured_expose(check_api_key=True) 979 def shutdown(self, **kwargs): 980 sabnzbd.shutdown_program() 981 return T("SABnzbd shutdown finished") 982 983 @secured_expose(check_api_key=True) 984 def pause(self, **kwargs): 985 sabnzbd.Scheduler.plan_resume(0) 986 sabnzbd.Downloader.pause() 987 raise queueRaiser(self.__root, kwargs) 988 989 @secured_expose(check_api_key=True) 990 def resume(self, **kwargs): 991 sabnzbd.Scheduler.plan_resume(0) 992 sabnzbd.unpause_all() 993 raise queueRaiser(self.__root, kwargs) 994 995 @secured_expose(check_api_key=True) 996 def pause_nzo(self, **kwargs): 997 uid = kwargs.get("uid", "") 998 sabnzbd.NzbQueue.pause_multiple_nzo(uid.split(",")) 999 raise queueRaiser(self.__root, kwargs) 1000 1001 @secured_expose(check_api_key=True) 1002 def resume_nzo(self, **kwargs): 1003 uid = kwargs.get("uid", "") 1004 sabnzbd.NzbQueue.resume_multiple_nzo(uid.split(",")) 1005 raise queueRaiser(self.__root, kwargs) 1006 1007 @secured_expose(check_api_key=True) 1008 def set_priority(self, **kwargs): 1009 sabnzbd.NzbQueue.set_priority(kwargs.get("nzo_id"), kwargs.get("priority")) 1010 raise queueRaiser(self.__root, kwargs) 1011 1012 @secured_expose(check_api_key=True) 1013 def sort_by_avg_age(self, **kwargs): 1014 sabnzbd.NzbQueue.sort_queue("avg_age", kwargs.get("dir")) 1015 raise queueRaiser(self.__root, kwargs) 1016 1017 @secured_expose(check_api_key=True) 1018 def sort_by_name(self, **kwargs): 1019 sabnzbd.NzbQueue.sort_queue("name", kwargs.get("dir")) 1020 raise queueRaiser(self.__root, kwargs) 1021 1022 @secured_expose(check_api_key=True) 1023 def sort_by_size(self, **kwargs): 1024 sabnzbd.NzbQueue.sort_queue("size", kwargs.get("dir")) 1025 raise queueRaiser(self.__root, kwargs) 1026 1027 1028############################################################################## 1029class HistoryPage: 1030 def __init__(self, root): 1031 self.__root = root 1032 1033 @secured_expose 1034 def index(self, **kwargs): 1035 start = int_conv(kwargs.get("start")) 1036 limit = int_conv(kwargs.get("limit")) 1037 search = kwargs.get("search") 1038 failed_only = int_conv(kwargs.get("failed_only")) 1039 1040 history = build_header() 1041 history["failed_only"] = failed_only 1042 history["rating_enable"] = bool(cfg.rating_enable()) 1043 1044 postfix = T("B") # : Abbreviation for bytes, as in GB 1045 grand, month, week, day = sabnzbd.BPSMeter.get_sums() 1046 history["total_size"], history["month_size"], history["week_size"], history["day_size"] = ( 1047 to_units(grand, postfix=postfix), 1048 to_units(month, postfix=postfix), 1049 to_units(week, postfix=postfix), 1050 to_units(day, postfix=postfix), 1051 ) 1052 1053 history["lines"], history["fetched"], history["noofslots"] = build_history( 1054 start=start, limit=limit, search=search, failed_only=failed_only 1055 ) 1056 1057 if search: 1058 history["search"] = escape(search) 1059 else: 1060 history["search"] = "" 1061 1062 history["start"] = int_conv(start) 1063 history["limit"] = int_conv(limit) 1064 history["finish"] = history["start"] + history["limit"] 1065 if history["finish"] > history["noofslots"]: 1066 history["finish"] = history["noofslots"] 1067 if not history["finish"]: 1068 history["finish"] = history["fetched"] 1069 history["time_format"] = time_format 1070 1071 template = Template( 1072 file=os.path.join(sabnzbd.WEB_DIR, "history.tmpl"), 1073 searchList=[history], 1074 compilerSettings=CHEETAH_DIRECTIVES, 1075 ) 1076 return template.respond() 1077 1078 @secured_expose(check_api_key=True) 1079 def purge(self, **kwargs): 1080 history_db = sabnzbd.get_db_connection() 1081 history_db.remove_history() 1082 raise queueRaiser(self.__root, kwargs) 1083 1084 @secured_expose(check_api_key=True) 1085 def delete(self, **kwargs): 1086 job = kwargs.get("job") 1087 del_files = int_conv(kwargs.get("del_files")) 1088 if job: 1089 jobs = job.split(",") 1090 for job in jobs: 1091 del_hist_job(job, del_files=del_files) 1092 raise queueRaiser(self.__root, kwargs) 1093 1094 @secured_expose(check_api_key=True) 1095 def retry_pp(self, **kwargs): 1096 retry_job(kwargs.get("job"), kwargs.get("nzbfile"), kwargs.get("password")) 1097 raise queueRaiser(self.__root, kwargs) 1098 1099 1100############################################################################## 1101class ConfigPage: 1102 def __init__(self, root): 1103 self.__root = root 1104 self.folders = ConfigFolders("/config/folders/") 1105 self.notify = ConfigNotify("/config/notify/") 1106 self.general = ConfigGeneral("/config/general/") 1107 self.rss = ConfigRss("/config/rss/") 1108 self.scheduling = ConfigScheduling("/config/scheduling/") 1109 self.server = ConfigServer("/config/server/") 1110 self.switches = ConfigSwitches("/config/switches/") 1111 self.categories = ConfigCats("/config/categories/") 1112 self.sorting = ConfigSorting("/config/sorting/") 1113 self.special = ConfigSpecial("/config/special/") 1114 1115 @secured_expose(check_configlock=True) 1116 def index(self, **kwargs): 1117 conf = build_header(sabnzbd.WEB_DIR_CONFIG) 1118 conf["configfn"] = clip_path(config.get_filename()) 1119 conf["cmdline"] = sabnzbd.CMDLINE 1120 conf["build"] = sabnzbd.__baseline__[:7] 1121 1122 conf["have_unzip"] = bool(sabnzbd.newsunpack.ZIP_COMMAND) 1123 conf["have_7zip"] = bool(sabnzbd.newsunpack.SEVEN_COMMAND) 1124 conf["have_sabyenc"] = sabnzbd.decoder.SABYENC_ENABLED 1125 conf["have_mt_par2"] = sabnzbd.newsunpack.PAR2_MT 1126 1127 conf["certificate_validation"] = sabnzbd.CERTIFICATE_VALIDATION 1128 conf["ssl_version"] = ssl.OPENSSL_VERSION 1129 1130 new = {} 1131 for svr in config.get_servers(): 1132 new[svr] = {} 1133 conf["servers"] = new 1134 1135 conf["folders"] = sabnzbd.NzbQueue.scan_jobs(all_jobs=False, action=False) 1136 1137 template = Template( 1138 file=os.path.join(sabnzbd.WEB_DIR_CONFIG, "config.tmpl"), 1139 searchList=[conf], 1140 compilerSettings=CHEETAH_DIRECTIVES, 1141 ) 1142 return template.respond() 1143 1144 @secured_expose(check_api_key=True) 1145 def restart(self, **kwargs): 1146 logging.info("Restart requested by interface") 1147 # Do the shutdown async to still send goodbye to browser 1148 Thread(target=sabnzbd.trigger_restart, kwargs={"timeout": 1}).start() 1149 return T( 1150 ' <br />SABnzbd shutdown finished.<br />Wait for about 5 second and then click the button below.<br /><br /><strong><a href="..">Refresh</a></strong><br />' 1151 ) 1152 1153 @secured_expose(check_api_key=True) 1154 def repair(self, **kwargs): 1155 logging.info("Queue repair requested by interface") 1156 sabnzbd.request_repair() 1157 # Do the shutdown async to still send goodbye to browser 1158 Thread(target=sabnzbd.trigger_restart, kwargs={"timeout": 1}).start() 1159 return T( 1160 ' <br />SABnzbd shutdown finished.<br />Wait for about 5 second and then click the button below.<br /><br /><strong><a href="..">Refresh</a></strong><br />' 1161 ) 1162 1163 1164############################################################################## 1165LIST_DIRPAGE = ( 1166 "download_dir", 1167 "download_free", 1168 "complete_dir", 1169 "complete_free", 1170 "admin_dir", 1171 "nzb_backup_dir", 1172 "dirscan_dir", 1173 "dirscan_speed", 1174 "script_dir", 1175 "email_dir", 1176 "permissions", 1177 "log_dir", 1178 "password_file", 1179) 1180 1181LIST_BOOL_DIRPAGE = ("fulldisk_autoresume",) 1182 1183 1184class ConfigFolders: 1185 def __init__(self, root): 1186 self.__root = root 1187 1188 @secured_expose(check_configlock=True) 1189 def index(self, **kwargs): 1190 conf = build_header(sabnzbd.WEB_DIR_CONFIG) 1191 1192 for kw in LIST_DIRPAGE + LIST_BOOL_DIRPAGE: 1193 conf[kw] = config.get_config("misc", kw)() 1194 1195 template = Template( 1196 file=os.path.join(sabnzbd.WEB_DIR_CONFIG, "config_folders.tmpl"), 1197 searchList=[conf], 1198 compilerSettings=CHEETAH_DIRECTIVES, 1199 ) 1200 return template.respond() 1201 1202 @secured_expose(check_api_key=True, check_configlock=True) 1203 def saveDirectories(self, **kwargs): 1204 for kw in LIST_DIRPAGE + LIST_BOOL_DIRPAGE: 1205 value = kwargs.get(kw) 1206 if value is not None or kw in LIST_BOOL_DIRPAGE: 1207 if kw in ("complete_dir", "dirscan_dir"): 1208 msg = config.get_config("misc", kw).set(value, create=True) 1209 else: 1210 msg = config.get_config("misc", kw).set(value) 1211 if msg: 1212 # return sabnzbd.api.report('json', error=msg) 1213 return badParameterResponse(msg, kwargs.get("ajax")) 1214 1215 if not sabnzbd.check_incomplete_vs_complete(): 1216 return badParameterResponse( 1217 T("The Completed Download Folder cannot be the same or a subfolder of the Temporary Download Folder"), 1218 kwargs.get("ajax"), 1219 ) 1220 config.save_config() 1221 if kwargs.get("ajax"): 1222 return sabnzbd.api.report("json") 1223 else: 1224 raise Raiser(self.__root) 1225 1226 1227############################################################################## 1228SWITCH_LIST = ( 1229 "par_option", 1230 "top_only", 1231 "direct_unpack", 1232 "enable_meta", 1233 "win_process_prio", 1234 "auto_sort", 1235 "propagation_delay", 1236 "auto_disconnect", 1237 "flat_unpack", 1238 "safe_postproc", 1239 "no_dupes", 1240 "replace_spaces", 1241 "replace_dots", 1242 "ignore_samples", 1243 "pause_on_post_processing", 1244 "nice", 1245 "ionice", 1246 "pre_script", 1247 "pause_on_pwrar", 1248 "sfv_check", 1249 "deobfuscate_final_filenames", 1250 "folder_rename", 1251 "load_balancing", 1252 "quota_size", 1253 "quota_day", 1254 "quota_resume", 1255 "quota_period", 1256 "history_retention", 1257 "pre_check", 1258 "max_art_tries", 1259 "fail_hopeless_jobs", 1260 "enable_all_par", 1261 "enable_recursive", 1262 "no_series_dupes", 1263 "series_propercheck", 1264 "script_can_fail", 1265 "new_nzb_on_failure", 1266 "unwanted_extensions", 1267 "action_on_unwanted_extensions", 1268 "unwanted_extensions_mode", 1269 "sanitize_safe", 1270 "rating_enable", 1271 "rating_api_key", 1272 "rating_filter_enable", 1273 "rating_filter_abort_audio", 1274 "rating_filter_abort_video", 1275 "rating_filter_abort_encrypted", 1276 "rating_filter_abort_encrypted_confirm", 1277 "rating_filter_abort_spam", 1278 "rating_filter_abort_spam_confirm", 1279 "rating_filter_abort_downvoted", 1280 "rating_filter_abort_keywords", 1281 "rating_filter_pause_audio", 1282 "rating_filter_pause_video", 1283 "rating_filter_pause_encrypted", 1284 "rating_filter_pause_encrypted_confirm", 1285 "rating_filter_pause_spam", 1286 "rating_filter_pause_spam_confirm", 1287 "rating_filter_pause_downvoted", 1288 "rating_filter_pause_keywords", 1289) 1290 1291 1292class ConfigSwitches: 1293 def __init__(self, root): 1294 self.__root = root 1295 1296 @secured_expose(check_configlock=True) 1297 def index(self, **kwargs): 1298 conf = build_header(sabnzbd.WEB_DIR_CONFIG) 1299 1300 conf["certificate_validation"] = sabnzbd.CERTIFICATE_VALIDATION 1301 conf["have_nice"] = bool(sabnzbd.newsunpack.NICE_COMMAND) 1302 conf["have_ionice"] = bool(sabnzbd.newsunpack.IONICE_COMMAND) 1303 conf["cleanup_list"] = cfg.cleanup_list.get_string() 1304 1305 for kw in SWITCH_LIST: 1306 conf[kw] = config.get_config("misc", kw)() 1307 conf["unwanted_extensions"] = cfg.unwanted_extensions.get_string() 1308 1309 conf["scripts"] = list_scripts() or ["None"] 1310 1311 template = Template( 1312 file=os.path.join(sabnzbd.WEB_DIR_CONFIG, "config_switches.tmpl"), 1313 searchList=[conf], 1314 compilerSettings=CHEETAH_DIRECTIVES, 1315 ) 1316 return template.respond() 1317 1318 @secured_expose(check_api_key=True, check_configlock=True) 1319 def saveSwitches(self, **kwargs): 1320 for kw in SWITCH_LIST: 1321 item = config.get_config("misc", kw) 1322 value = kwargs.get(kw) 1323 if kw == "unwanted_extensions" and value: 1324 value = value.lower().replace(".", "") 1325 msg = item.set(value) 1326 if msg: 1327 return badParameterResponse(msg, kwargs.get("ajax")) 1328 1329 cleanup_list = kwargs.get("cleanup_list") 1330 if cleanup_list and sabnzbd.WIN32: 1331 cleanup_list = cleanup_list.lower() 1332 cfg.cleanup_list.set(cleanup_list) 1333 1334 config.save_config() 1335 if kwargs.get("ajax"): 1336 return sabnzbd.api.report("json") 1337 else: 1338 raise Raiser(self.__root) 1339 1340 1341############################################################################## 1342SPECIAL_BOOL_LIST = ( 1343 "start_paused", 1344 "no_penalties", 1345 "fast_fail", 1346 "overwrite_files", 1347 "enable_par_cleanup", 1348 "queue_complete_pers", 1349 "api_warnings", 1350 "helpfull_warnings", 1351 "ampm", 1352 "enable_unrar", 1353 "enable_unzip", 1354 "enable_7zip", 1355 "enable_filejoin", 1356 "enable_tsjoin", 1357 "ignore_unrar_dates", 1358 "osx_menu", 1359 "osx_speed", 1360 "win_menu", 1361 "allow_incomplete_nzb", 1362 "rss_filenames", 1363 "ipv6_hosting", 1364 "keep_awake", 1365 "empty_postproc", 1366 "html_login", 1367 "wait_for_dfolder", 1368 "enable_broadcast", 1369 "warn_dupl_jobs", 1370 "replace_illegal", 1371 "backup_for_duplicates", 1372 "disable_api_key", 1373 "api_logging", 1374 "x_frame_options", 1375 "require_modern_tls", 1376) 1377SPECIAL_VALUE_LIST = ( 1378 "downloader_sleep_time", 1379 "size_limit", 1380 "movie_rename_limit", 1381 "nomedia_marker", 1382 "max_url_retries", 1383 "req_completion_rate", 1384 "wait_ext_drive", 1385 "max_foldername_length", 1386 "show_sysload", 1387 "url_base", 1388 "num_decoders", 1389 "direct_unpack_threads", 1390 "ipv6_servers", 1391 "selftest_host", 1392 "rating_host", 1393 "ssdp_broadcast_interval", 1394) 1395SPECIAL_LIST_LIST = ( 1396 "rss_odd_titles", 1397 "quick_check_ext_ignore", 1398 "host_whitelist", 1399 "local_ranges", 1400) 1401 1402 1403class ConfigSpecial: 1404 def __init__(self, root): 1405 self.__root = root 1406 1407 @secured_expose(check_configlock=True) 1408 def index(self, **kwargs): 1409 conf = build_header(sabnzbd.WEB_DIR_CONFIG) 1410 conf["switches"] = [ 1411 (kw, config.get_config("misc", kw)(), config.get_config("misc", kw).default()) for kw in SPECIAL_BOOL_LIST 1412 ] 1413 conf["entries"] = [ 1414 (kw, config.get_config("misc", kw)(), config.get_config("misc", kw).default()) for kw in SPECIAL_VALUE_LIST 1415 ] 1416 conf["entries"].extend( 1417 [ 1418 (kw, config.get_config("misc", kw).get_string(), config.get_config("misc", kw).default_string()) 1419 for kw in SPECIAL_LIST_LIST 1420 ] 1421 ) 1422 1423 template = Template( 1424 file=os.path.join(sabnzbd.WEB_DIR_CONFIG, "config_special.tmpl"), 1425 searchList=[conf], 1426 compilerSettings=CHEETAH_DIRECTIVES, 1427 ) 1428 return template.respond() 1429 1430 @secured_expose(check_api_key=True, check_configlock=True) 1431 def saveSpecial(self, **kwargs): 1432 for kw in SPECIAL_BOOL_LIST + SPECIAL_VALUE_LIST + SPECIAL_LIST_LIST: 1433 item = config.get_config("misc", kw) 1434 value = kwargs.get(kw) 1435 msg = item.set(value) 1436 if msg: 1437 return badParameterResponse(msg) 1438 1439 config.save_config() 1440 raise Raiser(self.__root) 1441 1442 1443############################################################################## 1444GENERAL_LIST = ( 1445 "host", 1446 "port", 1447 "username", 1448 "refresh_rate", 1449 "language", 1450 "cache_limit", 1451 "inet_exposure", 1452 "enable_https", 1453 "https_port", 1454 "https_cert", 1455 "https_key", 1456 "https_chain", 1457 "enable_https_verification", 1458 "auto_browser", 1459 "check_new_rel", 1460) 1461 1462 1463class ConfigGeneral: 1464 def __init__(self, root): 1465 self.__root = root 1466 1467 @secured_expose(check_configlock=True) 1468 def index(self, **kwargs): 1469 def ListColors(web_dir): 1470 lst = [] 1471 web_dir = os.path.join(sabnzbd.DIR_INTERFACES, web_dir) 1472 dd = os.path.abspath(web_dir + "/templates/static/stylesheets/colorschemes") 1473 if (not dd) or (not os.access(dd, os.R_OK)): 1474 return lst 1475 for color in globber(dd): 1476 col = color.replace(".css", "") 1477 lst.append(col) 1478 return lst 1479 1480 def add_color(skin_dir, color): 1481 if skin_dir: 1482 if not color: 1483 try: 1484 color = DEF_SKIN_COLORS[skin_dir.lower()] 1485 except KeyError: 1486 return skin_dir 1487 return "%s - %s" % (skin_dir, color) 1488 else: 1489 return "" 1490 1491 conf = build_header(sabnzbd.WEB_DIR_CONFIG) 1492 1493 conf["configfn"] = config.get_filename() 1494 conf["certificate_validation"] = sabnzbd.CERTIFICATE_VALIDATION 1495 1496 wlist = [] 1497 interfaces = globber_full(sabnzbd.DIR_INTERFACES) 1498 for k in interfaces: 1499 if k.endswith(DEF_STDCONFIG): 1500 interfaces.remove(k) 1501 continue 1502 1503 for web in interfaces: 1504 rweb = os.path.basename(web) 1505 if os.access(os.path.join(web, DEF_MAIN_TMPL), os.R_OK): 1506 cols = ListColors(rweb) 1507 if cols: 1508 for col in cols: 1509 wlist.append(add_color(rweb, col)) 1510 else: 1511 wlist.append(rweb) 1512 conf["web_list"] = wlist 1513 conf["web_dir"] = add_color(cfg.web_dir(), cfg.web_color()) 1514 conf["password"] = cfg.password.get_stars() 1515 1516 conf["language"] = cfg.language() 1517 lang_list = list_languages() 1518 if len(lang_list) < 2: 1519 lang_list = [] 1520 conf["lang_list"] = lang_list 1521 1522 for kw in GENERAL_LIST: 1523 conf[kw] = config.get_config("misc", kw)() 1524 1525 conf["bandwidth_max"] = cfg.bandwidth_max() 1526 conf["bandwidth_perc"] = cfg.bandwidth_perc() 1527 conf["nzb_key"] = cfg.nzb_key() 1528 conf["my_lcldata"] = cfg.admin_dir.get_clipped_path() 1529 conf["caller_url"] = cherrypy.request.base + cfg.url_base() 1530 1531 template = Template( 1532 file=os.path.join(sabnzbd.WEB_DIR_CONFIG, "config_general.tmpl"), 1533 searchList=[conf], 1534 compilerSettings=CHEETAH_DIRECTIVES, 1535 ) 1536 return template.respond() 1537 1538 @secured_expose(check_api_key=True, check_configlock=True) 1539 def saveGeneral(self, **kwargs): 1540 # Handle general options 1541 for kw in GENERAL_LIST: 1542 item = config.get_config("misc", kw) 1543 value = kwargs.get(kw) 1544 msg = item.set(value) 1545 if msg: 1546 return badParameterResponse(msg) 1547 1548 # Handle special options 1549 cfg.password.set(kwargs.get("password")) 1550 1551 web_dir = kwargs.get("web_dir") 1552 change_web_dir(web_dir) 1553 1554 bandwidth_max = kwargs.get("bandwidth_max") 1555 if bandwidth_max is not None: 1556 cfg.bandwidth_max.set(bandwidth_max) 1557 bandwidth_perc = kwargs.get("bandwidth_perc") 1558 if bandwidth_perc is not None: 1559 cfg.bandwidth_perc.set(bandwidth_perc) 1560 bandwidth_perc = cfg.bandwidth_perc() 1561 if bandwidth_perc and not bandwidth_max: 1562 logging.warning_helpful(T("You must set a maximum bandwidth before you can set a bandwidth limit")) 1563 1564 config.save_config() 1565 1566 # Update CherryPy authentication 1567 set_auth(cherrypy.config) 1568 if kwargs.get("ajax"): 1569 return sabnzbd.api.report("json", data={"success": True, "restart_req": sabnzbd.RESTART_REQ}) 1570 else: 1571 raise Raiser(self.__root) 1572 1573 1574def change_web_dir(web_dir): 1575 try: 1576 web_dir, web_color = web_dir.split(" - ") 1577 except: 1578 try: 1579 web_color = DEF_SKIN_COLORS[web_dir.lower()] 1580 except: 1581 web_color = "" 1582 1583 web_dir_path = real_path(sabnzbd.DIR_INTERFACES, web_dir) 1584 1585 if not os.path.exists(web_dir_path): 1586 return badParameterResponse("Cannot find web template: %s" % web_dir_path) 1587 else: 1588 cfg.web_dir.set(web_dir) 1589 cfg.web_color.set(web_color) 1590 1591 1592############################################################################## 1593class ConfigServer: 1594 def __init__(self, root): 1595 self.__root = root 1596 1597 @secured_expose(check_configlock=True) 1598 def index(self, **kwargs): 1599 conf = build_header(sabnzbd.WEB_DIR_CONFIG) 1600 new = [] 1601 servers = config.get_servers() 1602 server_names = sorted( 1603 servers, 1604 key=lambda svr: "%d%02d%s" 1605 % (int(not servers[svr].enable()), servers[svr].priority(), servers[svr].displayname().lower()), 1606 ) 1607 for svr in server_names: 1608 new.append(servers[svr].get_dict(safe=True)) 1609 t, m, w, d, daily, articles_tried, articles_success = sabnzbd.BPSMeter.amounts(svr) 1610 if t: 1611 new[-1]["amounts"] = ( 1612 to_units(t), 1613 to_units(m), 1614 to_units(w), 1615 to_units(d), 1616 daily, 1617 articles_tried, 1618 articles_success, 1619 ) 1620 new[-1]["quota_left"] = to_units( 1621 servers[svr].quota.get_int() - sabnzbd.BPSMeter.grand_total.get(svr, 0) + servers[svr].usage_at_start() 1622 ) 1623 1624 conf["servers"] = new 1625 conf["cats"] = list_cats(default=True) 1626 conf["certificate_validation"] = sabnzbd.CERTIFICATE_VALIDATION 1627 1628 template = Template( 1629 file=os.path.join(sabnzbd.WEB_DIR_CONFIG, "config_server.tmpl"), 1630 searchList=[conf], 1631 compilerSettings=CHEETAH_DIRECTIVES, 1632 ) 1633 return template.respond() 1634 1635 @secured_expose(check_api_key=True, check_configlock=True) 1636 def addServer(self, **kwargs): 1637 return handle_server(kwargs, self.__root, True) 1638 1639 @secured_expose(check_api_key=True, check_configlock=True) 1640 def saveServer(self, **kwargs): 1641 return handle_server(kwargs, self.__root) 1642 1643 @secured_expose(check_api_key=True, check_configlock=True) 1644 def testServer(self, **kwargs): 1645 return handle_server_test(kwargs, self.__root) 1646 1647 @secured_expose(check_api_key=True, check_configlock=True) 1648 def delServer(self, **kwargs): 1649 kwargs["section"] = "servers" 1650 kwargs["keyword"] = kwargs.get("server") 1651 del_from_section(kwargs) 1652 raise Raiser(self.__root) 1653 1654 @secured_expose(check_api_key=True, check_configlock=True) 1655 def clrServer(self, **kwargs): 1656 server = kwargs.get("server") 1657 if server: 1658 sabnzbd.BPSMeter.clear_server(server) 1659 raise Raiser(self.__root) 1660 1661 @secured_expose(check_api_key=True, check_configlock=True) 1662 def toggleServer(self, **kwargs): 1663 server = kwargs.get("server") 1664 if server: 1665 svr = config.get_config("servers", server) 1666 if svr: 1667 svr.enable.set(not svr.enable()) 1668 config.save_config() 1669 sabnzbd.Downloader.update_server(server, server) 1670 raise Raiser(self.__root) 1671 1672 1673def unique_svr_name(server): 1674 """Return a unique variant on given server name""" 1675 num = 0 1676 svr = 1 1677 new_name = server 1678 while svr: 1679 if num: 1680 new_name = "%s@%d" % (server, num) 1681 else: 1682 new_name = "%s" % server 1683 svr = config.get_config("servers", new_name) 1684 num += 1 1685 return new_name 1686 1687 1688def check_server(host, port, ajax): 1689 """Check if server address resolves properly""" 1690 if host.lower() == "localhost" and sabnzbd.AMBI_LOCALHOST: 1691 return badParameterResponse(T("Warning: LOCALHOST is ambiguous, use numerical IP-address."), ajax) 1692 1693 if get_server_addrinfo(host, int_conv(port)): 1694 return "" 1695 else: 1696 return badParameterResponse(T('Server address "%s:%s" is not valid.') % (host, port), ajax) 1697 1698 1699def handle_server(kwargs, root=None, new_svr=False): 1700 """Internal server handler""" 1701 ajax = kwargs.get("ajax") 1702 host = kwargs.get("host", "").strip() 1703 if not host: 1704 return badParameterResponse(T("Server address required"), ajax) 1705 1706 port = kwargs.get("port", "").strip() 1707 if not port: 1708 if not kwargs.get("ssl", "").strip(): 1709 port = "119" 1710 else: 1711 port = "563" 1712 kwargs["port"] = port 1713 1714 if kwargs.get("connections", "").strip() == "": 1715 kwargs["connections"] = "1" 1716 1717 if kwargs.get("enable") == "1": 1718 msg = check_server(host, port, ajax) 1719 if msg: 1720 return msg 1721 1722 # Default server name is just the host name 1723 server = host 1724 1725 svr = None 1726 old_server = kwargs.get("server") 1727 if old_server: 1728 svr = config.get_config("servers", old_server) 1729 if svr: 1730 server = old_server 1731 else: 1732 svr = config.get_config("servers", server) 1733 1734 if new_svr: 1735 server = unique_svr_name(server) 1736 1737 for kw in ("ssl", "send_group", "enable", "optional"): 1738 if kw not in kwargs.keys(): 1739 kwargs[kw] = None 1740 if svr and not new_svr: 1741 svr.set_dict(kwargs) 1742 else: 1743 old_server = None 1744 config.ConfigServer(server, kwargs) 1745 1746 config.save_config() 1747 sabnzbd.Downloader.update_server(old_server, server) 1748 if root: 1749 if ajax: 1750 return sabnzbd.api.report("json") 1751 else: 1752 raise Raiser(root) 1753 1754 1755def handle_server_test(kwargs, root): 1756 _result, msg = test_nntp_server_dict(kwargs) 1757 return msg 1758 1759 1760############################################################################## 1761class ConfigRss: 1762 def __init__(self, root): 1763 self.__root = root 1764 self.__refresh_readout = None # Set to URL when new readout is needed 1765 self.__refresh_download = False # True when feed needs to be read 1766 self.__refresh_force = False # True if forced download of all matches is required 1767 self.__refresh_ignore = False # True if first batch of new feed must be ignored 1768 self.__evaluate = False # True if feed needs to be re-filtered 1769 self.__show_eval_button = False # True if the "Apply filers" button should be shown 1770 self.__last_msg = "" # Last error message from RSS reader 1771 1772 @secured_expose(check_configlock=True) 1773 def index(self, **kwargs): 1774 conf = build_header(sabnzbd.WEB_DIR_CONFIG) 1775 1776 conf["scripts"] = list_scripts(default=True) 1777 pick_script = conf["scripts"] != [] 1778 1779 conf["categories"] = list_cats(default=True) 1780 pick_cat = conf["categories"] != [] 1781 1782 conf["rss_rate"] = cfg.rss_rate() 1783 1784 rss = {} 1785 feeds = config.get_rss() 1786 for feed in feeds: 1787 rss[feed] = feeds[feed].get_dict() 1788 filters = feeds[feed].filters() 1789 rss[feed]["filters"] = filters 1790 rss[feed]["filter_states"] = [bool(sabnzbd.rss.convert_filter(f[4])) for f in filters] 1791 rss[feed]["filtercount"] = len(filters) 1792 1793 rss[feed]["pick_cat"] = pick_cat 1794 rss[feed]["pick_script"] = pick_script 1795 rss[feed]["link"] = urllib.parse.quote_plus(feed) 1796 rss[feed]["baselink"] = [get_base_url(uri) for uri in rss[feed]["uri"]] 1797 rss[feed]["uris"] = feeds[feed].uri.get_string() 1798 1799 active_feed = kwargs.get("feed", "") 1800 conf["active_feed"] = active_feed 1801 conf["rss"] = rss 1802 conf["rss_next"] = time.strftime(time_format("%H:%M"), time.localtime(sabnzbd.RSSReader.next_run)) 1803 1804 if active_feed: 1805 readout = bool(self.__refresh_readout) 1806 logging.debug("RSS READOUT = %s", readout) 1807 if not readout: 1808 self.__refresh_download = False 1809 self.__refresh_force = False 1810 self.__refresh_ignore = False 1811 if self.__evaluate: 1812 msg = sabnzbd.RSSReader.run_feed( 1813 active_feed, 1814 download=self.__refresh_download, 1815 force=self.__refresh_force, 1816 ignoreFirst=self.__refresh_ignore, 1817 readout=readout, 1818 ) 1819 else: 1820 msg = "" 1821 self.__evaluate = False 1822 if readout: 1823 sabnzbd.RSSReader.save() 1824 self.__last_msg = msg 1825 else: 1826 msg = self.__last_msg 1827 self.__refresh_readout = None 1828 conf["evalButton"] = self.__show_eval_button 1829 conf["error"] = msg 1830 1831 conf["downloaded"], conf["matched"], conf["unmatched"] = GetRssLog(active_feed) 1832 else: 1833 self.__last_msg = "" 1834 1835 # Find a unique new Feed name 1836 unum = 1 1837 txt = T("Feed") # : Used as default Feed name in Config->RSS 1838 while txt + str(unum) in feeds: 1839 unum += 1 1840 conf["feed"] = txt + str(unum) 1841 1842 template = Template( 1843 file=os.path.join(sabnzbd.WEB_DIR_CONFIG, "config_rss.tmpl"), 1844 searchList=[conf], 1845 compilerSettings=CHEETAH_DIRECTIVES, 1846 ) 1847 return template.respond() 1848 1849 @secured_expose(check_api_key=True, check_configlock=True) 1850 def save_rss_rate(self, **kwargs): 1851 """Save changed RSS automatic readout rate""" 1852 cfg.rss_rate.set(kwargs.get("rss_rate")) 1853 config.save_config() 1854 sabnzbd.Scheduler.restart() 1855 raise Raiser(self.__root) 1856 1857 @secured_expose(check_api_key=True, check_configlock=True) 1858 def upd_rss_feed(self, **kwargs): 1859 """Update Feed level attributes, 1860 legacy version: ignores 'enable' parameter 1861 """ 1862 if kwargs.get("enable") is not None: 1863 del kwargs["enable"] 1864 try: 1865 cf = config.get_rss()[kwargs.get("feed")] 1866 except KeyError: 1867 cf = None 1868 uri = Strip(kwargs.get("uri")) 1869 if cf and uri: 1870 kwargs["uri"] = uri 1871 cf.set_dict(kwargs) 1872 config.save_config() 1873 1874 self.__evaluate = False 1875 self.__show_eval_button = True 1876 raise rssRaiser(self.__root, kwargs) 1877 1878 @secured_expose(check_api_key=True, check_configlock=True) 1879 def save_rss_feed(self, **kwargs): 1880 """Update Feed level attributes""" 1881 feed_name = kwargs.get("feed") 1882 try: 1883 cf = config.get_rss()[feed_name] 1884 except KeyError: 1885 cf = None 1886 if "enable" not in kwargs: 1887 kwargs["enable"] = 0 1888 uri = Strip(kwargs.get("uri")) 1889 if cf and uri: 1890 kwargs["uri"] = uri 1891 cf.set_dict(kwargs) 1892 1893 # Did we get a new name for this feed? 1894 new_name = kwargs.get("feed_new_name") 1895 if new_name and new_name != feed_name: 1896 cf.rename(new_name) 1897 # Update the feed name for the redirect 1898 kwargs["feed"] = new_name 1899 1900 config.save_config() 1901 1902 raise rssRaiser(self.__root, kwargs) 1903 1904 @secured_expose(check_api_key=True, check_configlock=True) 1905 def toggle_rss_feed(self, **kwargs): 1906 """Toggle automatic read-out flag of Feed""" 1907 try: 1908 item = config.get_rss()[kwargs.get("feed")] 1909 except KeyError: 1910 item = None 1911 if cfg: 1912 item.enable.set(not item.enable()) 1913 config.save_config() 1914 if kwargs.get("table"): 1915 raise Raiser(self.__root) 1916 else: 1917 raise rssRaiser(self.__root, kwargs) 1918 1919 @secured_expose(check_api_key=True, check_configlock=True) 1920 def add_rss_feed(self, **kwargs): 1921 """Add one new RSS feed definition""" 1922 feed = Strip(kwargs.get("feed")).strip("[]") 1923 uri = Strip(kwargs.get("uri")) 1924 if feed and uri: 1925 try: 1926 cfg = config.get_rss()[feed] 1927 except KeyError: 1928 cfg = None 1929 if (not cfg) and uri: 1930 kwargs["feed"] = feed 1931 kwargs["uri"] = uri 1932 config.ConfigRSS(feed, kwargs) 1933 # Clear out any existing reference to this feed name 1934 # Otherwise first-run detection can fail 1935 sabnzbd.RSSReader.clear_feed(feed) 1936 config.save_config() 1937 self.__refresh_readout = feed 1938 self.__refresh_download = False 1939 self.__refresh_force = False 1940 self.__refresh_ignore = True 1941 self.__evaluate = True 1942 raise rssRaiser(self.__root, kwargs) 1943 else: 1944 raise Raiser(self.__root) 1945 else: 1946 raise Raiser(self.__root) 1947 1948 @secured_expose(check_api_key=True, check_configlock=True) 1949 def upd_rss_filter(self, **kwargs): 1950 """Wrapper, so we can call from api.py""" 1951 self.internal_upd_rss_filter(**kwargs) 1952 1953 def internal_upd_rss_filter(self, **kwargs): 1954 """Save updated filter definition""" 1955 try: 1956 feed_cfg = config.get_rss()[kwargs.get("feed")] 1957 except KeyError: 1958 raise rssRaiser(self.__root, kwargs) 1959 1960 pp = kwargs.get("pp") 1961 if IsNone(pp): 1962 pp = "" 1963 script = ConvertSpecials(kwargs.get("script")) 1964 cat = ConvertSpecials(kwargs.get("cat")) 1965 prio = ConvertSpecials(kwargs.get("priority")) 1966 filt = kwargs.get("filter_text") 1967 enabled = kwargs.get("enabled", "0") 1968 1969 if filt: 1970 feed_cfg.filters.update( 1971 int(kwargs.get("index", 0)), (cat, pp, script, kwargs.get("filter_type"), filt, prio, enabled) 1972 ) 1973 1974 # Move filter if requested 1975 index = int_conv(kwargs.get("index", "")) 1976 new_index = kwargs.get("new_index", "") 1977 if new_index and int_conv(new_index) != index: 1978 feed_cfg.filters.move(int(index), int_conv(new_index)) 1979 1980 config.save_config() 1981 self.__evaluate = False 1982 self.__show_eval_button = True 1983 raise rssRaiser(self.__root, kwargs) 1984 1985 @secured_expose(check_api_key=True, check_configlock=True) 1986 def del_rss_feed(self, *args, **kwargs): 1987 """Remove complete RSS feed""" 1988 kwargs["section"] = "rss" 1989 kwargs["keyword"] = kwargs.get("feed") 1990 del_from_section(kwargs) 1991 sabnzbd.RSSReader.clear_feed(kwargs.get("feed")) 1992 raise Raiser(self.__root) 1993 1994 @secured_expose(check_api_key=True, check_configlock=True) 1995 def del_rss_filter(self, **kwargs): 1996 """Wrapper, so we can call from api.py""" 1997 self.internal_del_rss_filter(**kwargs) 1998 1999 def internal_del_rss_filter(self, **kwargs): 2000 """Remove one RSS filter""" 2001 try: 2002 feed_cfg = config.get_rss()[kwargs.get("feed")] 2003 except KeyError: 2004 raise rssRaiser(self.__root, kwargs) 2005 2006 feed_cfg.filters.delete(int(kwargs.get("index", 0))) 2007 config.save_config() 2008 self.__evaluate = False 2009 self.__show_eval_button = True 2010 raise rssRaiser(self.__root, kwargs) 2011 2012 @secured_expose(check_api_key=True, check_configlock=True) 2013 def download_rss_feed(self, *args, **kwargs): 2014 """Force download of all matching jobs in a feed""" 2015 if "feed" in kwargs: 2016 feed = kwargs["feed"] 2017 self.__refresh_readout = feed 2018 self.__refresh_download = True 2019 self.__refresh_force = True 2020 self.__refresh_ignore = False 2021 self.__evaluate = True 2022 raise rssRaiser(self.__root, kwargs) 2023 2024 @secured_expose(check_api_key=True, check_configlock=True) 2025 def clean_rss_jobs(self, *args, **kwargs): 2026 """Remove processed RSS jobs from UI""" 2027 sabnzbd.RSSReader.clear_downloaded(kwargs["feed"]) 2028 self.__evaluate = True 2029 raise rssRaiser(self.__root, kwargs) 2030 2031 @secured_expose(check_api_key=True, check_configlock=True) 2032 def test_rss_feed(self, *args, **kwargs): 2033 """Read the feed content again and show results""" 2034 if "feed" in kwargs: 2035 feed = kwargs["feed"] 2036 self.__refresh_readout = feed 2037 self.__refresh_download = False 2038 self.__refresh_force = False 2039 self.__refresh_ignore = True 2040 self.__evaluate = True 2041 self.__show_eval_button = False 2042 raise rssRaiser(self.__root, kwargs) 2043 2044 @secured_expose(check_api_key=True, check_configlock=True) 2045 def eval_rss_feed(self, *args, **kwargs): 2046 """Re-apply the filters to the feed""" 2047 if "feed" in kwargs: 2048 self.__refresh_download = False 2049 self.__refresh_force = False 2050 self.__refresh_ignore = False 2051 self.__show_eval_button = False 2052 self.__evaluate = True 2053 2054 raise rssRaiser(self.__root, kwargs) 2055 2056 @secured_expose(check_api_key=True, check_configlock=True) 2057 def download(self, **kwargs): 2058 """Download NZB from provider (Download button)""" 2059 feed = kwargs.get("feed") 2060 url = kwargs.get("url") 2061 nzbname = kwargs.get("nzbname") 2062 att = sabnzbd.RSSReader.lookup_url(feed, url) 2063 if att: 2064 pp = att.get("pp") 2065 cat = att.get("cat") 2066 script = att.get("script") 2067 prio = att.get("prio") 2068 2069 if url: 2070 sabnzbd.add_url(url, pp, script, cat, prio, nzbname) 2071 # Need to pass the title instead 2072 sabnzbd.RSSReader.flag_downloaded(feed, url) 2073 raise rssRaiser(self.__root, kwargs) 2074 2075 @secured_expose(check_api_key=True, check_configlock=True) 2076 def rss_now(self, *args, **kwargs): 2077 """Run an automatic RSS run now""" 2078 sabnzbd.Scheduler.force_rss() 2079 raise Raiser(self.__root) 2080 2081 2082def ConvertSpecials(p): 2083 """Convert None to 'None' and 'Default' to ''""" 2084 if p is None: 2085 p = "None" 2086 elif p.lower() == T("Default").lower(): 2087 p = "" 2088 return p 2089 2090 2091def IsNone(value): 2092 """Return True if either None, 'None' or ''""" 2093 return value is None or value == "" or value.lower() == "none" 2094 2095 2096def Strip(txt): 2097 """Return stripped string, can handle None""" 2098 try: 2099 return txt.strip() 2100 except: 2101 return None 2102 2103 2104############################################################################## 2105_SCHED_ACTIONS = ( 2106 "resume", 2107 "pause", 2108 "pause_all", 2109 "shutdown", 2110 "restart", 2111 "speedlimit", 2112 "pause_post", 2113 "resume_post", 2114 "scan_folder", 2115 "rss_scan", 2116 "remove_failed", 2117 "remove_completed", 2118 "pause_all_low", 2119 "pause_all_normal", 2120 "pause_all_high", 2121 "resume_all_low", 2122 "resume_all_normal", 2123 "resume_all_high", 2124 "enable_quota", 2125 "disable_quota", 2126) 2127 2128 2129class ConfigScheduling: 2130 def __init__(self, root): 2131 self.__root = root 2132 2133 @secured_expose(check_configlock=True) 2134 def index(self, **kwargs): 2135 def get_days(): 2136 days = { 2137 "*": T("Daily"), 2138 "1": T("Monday"), 2139 "2": T("Tuesday"), 2140 "3": T("Wednesday"), 2141 "4": T("Thursday"), 2142 "5": T("Friday"), 2143 "6": T("Saturday"), 2144 "7": T("Sunday"), 2145 } 2146 return days 2147 2148 conf = build_header(sabnzbd.WEB_DIR_CONFIG) 2149 2150 actions = [] 2151 actions.extend(_SCHED_ACTIONS) 2152 day_names = get_days() 2153 categories = list_cats(False) 2154 snum = 1 2155 conf["schedlines"] = [] 2156 conf["taskinfo"] = [] 2157 for ev in sabnzbd.scheduler.sort_schedules(all_events=False): 2158 line = ev[3] 2159 conf["schedlines"].append(line) 2160 try: 2161 enabled, m, h, day_numbers, action = line.split(" ", 4) 2162 except: 2163 continue 2164 action = action.strip() 2165 try: 2166 action, value = action.split(" ", 1) 2167 except: 2168 value = "" 2169 value = value.strip() 2170 if value and not value.lower().strip("0123456789kmgtp%."): 2171 if "%" not in value and from_units(value) < 1.0: 2172 value = T("off") # : "Off" value for speedlimit in scheduler 2173 else: 2174 if "%" not in value and 1 < int_conv(value) < 101: 2175 value += "%" 2176 value = value.upper() 2177 if action in actions: 2178 action = Ttemplate("sch-" + action) 2179 else: 2180 if action in ("enable_server", "disable_server"): 2181 try: 2182 value = '"%s"' % config.get_servers()[value].displayname() 2183 except KeyError: 2184 value = '"%s" <<< %s' % (value, T("Undefined server!")) 2185 action = Ttemplate("sch-" + action) 2186 if action in ("pause_cat", "resume_cat"): 2187 action = Ttemplate("sch-" + action) 2188 if value not in categories: 2189 # Category name change 2190 value = '"%s" <<< %s' % (value, T("Incorrect parameter")) 2191 else: 2192 value = '"%s"' % value 2193 2194 if day_numbers == "1234567": 2195 days_of_week = "Daily" 2196 elif day_numbers == "12345": 2197 days_of_week = "Weekdays" 2198 elif day_numbers == "67": 2199 days_of_week = "Weekends" 2200 else: 2201 days_of_week = ", ".join([day_names.get(i, "**") for i in day_numbers]) 2202 2203 item = (snum, "%02d" % int(h), "%02d" % int(m), days_of_week, "%s %s" % (action, value), enabled) 2204 2205 conf["taskinfo"].append(item) 2206 snum += 1 2207 2208 actions_lng = {} 2209 for action in actions: 2210 actions_lng[action] = Ttemplate("sch-" + action) 2211 2212 actions_servers = {} 2213 servers = config.get_servers() 2214 for srv in servers: 2215 actions_servers[srv] = servers[srv].displayname() 2216 2217 conf["actions_servers"] = actions_servers 2218 conf["actions"] = actions 2219 conf["actions_lng"] = actions_lng 2220 conf["categories"] = categories 2221 2222 template = Template( 2223 file=os.path.join(sabnzbd.WEB_DIR_CONFIG, "config_scheduling.tmpl"), 2224 searchList=[conf], 2225 compilerSettings=CHEETAH_DIRECTIVES, 2226 ) 2227 return template.respond() 2228 2229 @secured_expose(check_api_key=True, check_configlock=True) 2230 def addSchedule(self, **kwargs): 2231 servers = config.get_servers() 2232 minute = kwargs.get("minute") 2233 hour = kwargs.get("hour") 2234 days_of_week = "".join([str(x) for x in kwargs.get("daysofweek", "")]) 2235 if not days_of_week: 2236 days_of_week = "1234567" 2237 action = kwargs.get("action") 2238 arguments = kwargs.get("arguments") 2239 2240 arguments = arguments.strip().lower() 2241 if arguments in ("on", "enable"): 2242 arguments = "1" 2243 elif arguments in ("off", "disable"): 2244 arguments = "0" 2245 2246 if minute and hour and days_of_week and action: 2247 if action == "speedlimit": 2248 if not arguments or arguments.strip("0123456789kmgtp%."): 2249 arguments = 0 2250 elif action in _SCHED_ACTIONS: 2251 arguments = "" 2252 elif action in servers: 2253 if arguments == "1": 2254 arguments = action 2255 action = "enable_server" 2256 else: 2257 arguments = action 2258 action = "disable_server" 2259 2260 elif action in ("pause_cat", "resume_cat"): 2261 # Need original category name, not lowercased 2262 arguments = arguments.strip() 2263 else: 2264 # Something else, leave empty 2265 action = None 2266 2267 if action: 2268 sched = cfg.schedules() 2269 sched.append("%s %s %s %s %s %s" % (1, minute, hour, days_of_week, action, arguments)) 2270 cfg.schedules.set(sched) 2271 2272 config.save_config() 2273 sabnzbd.Scheduler.restart() 2274 raise Raiser(self.__root) 2275 2276 @secured_expose(check_api_key=True, check_configlock=True) 2277 def delSchedule(self, **kwargs): 2278 schedules = cfg.schedules() 2279 line = kwargs.get("line") 2280 if line and line in schedules: 2281 schedules.remove(line) 2282 cfg.schedules.set(schedules) 2283 config.save_config() 2284 sabnzbd.Scheduler.restart() 2285 raise Raiser(self.__root) 2286 2287 @secured_expose(check_api_key=True, check_configlock=True) 2288 def toggleSchedule(self, **kwargs): 2289 schedules = cfg.schedules() 2290 line = kwargs.get("line") 2291 if line: 2292 for i, schedule in enumerate(schedules): 2293 if schedule == line: 2294 # Toggle the schedule 2295 schedule_split = schedule.split() 2296 schedule_split[0] = "%d" % (schedule_split[0] == "0") 2297 schedules[i] = " ".join(schedule_split) 2298 break 2299 cfg.schedules.set(schedules) 2300 config.save_config() 2301 sabnzbd.Scheduler.restart() 2302 raise Raiser(self.__root) 2303 2304 2305############################################################################## 2306class ConfigCats: 2307 def __init__(self, root): 2308 self.__root = root 2309 2310 @secured_expose(check_configlock=True) 2311 def index(self, **kwargs): 2312 conf = build_header(sabnzbd.WEB_DIR_CONFIG) 2313 2314 conf["scripts"] = list_scripts(default=True) 2315 conf["defdir"] = cfg.complete_dir.get_clipped_path() 2316 2317 categories = config.get_ordered_categories() 2318 conf["have_cats"] = len(categories) > 1 2319 2320 slotinfo = [] 2321 for cat in categories: 2322 cat["newzbin"] = cat["newzbin"].replace('"', """) 2323 slotinfo.append(cat) 2324 2325 # Add empty line 2326 empty = { 2327 "name": "", 2328 "order": "0", 2329 "pp": "-1", 2330 "script": "", 2331 "dir": "", 2332 "newzbin": "", 2333 "priority": DEFAULT_PRIORITY, 2334 } 2335 slotinfo.insert(1, empty) 2336 conf["slotinfo"] = slotinfo 2337 2338 template = Template( 2339 file=os.path.join(sabnzbd.WEB_DIR_CONFIG, "config_cat.tmpl"), 2340 searchList=[conf], 2341 compilerSettings=CHEETAH_DIRECTIVES, 2342 ) 2343 return template.respond() 2344 2345 @secured_expose(check_api_key=True, check_configlock=True) 2346 def delete(self, **kwargs): 2347 kwargs["section"] = "categories" 2348 kwargs["keyword"] = kwargs.get("name") 2349 del_from_section(kwargs) 2350 raise Raiser(self.__root) 2351 2352 @secured_expose(check_api_key=True, check_configlock=True) 2353 def save(self, **kwargs): 2354 name = kwargs.get("name", "*") 2355 if name == "*": 2356 newname = name 2357 else: 2358 newname = re.sub('"', "", kwargs.get("newname", "")) 2359 if newname: 2360 # Check if this cat-dir is not sub-folder of incomplete 2361 if same_file(cfg.download_dir.get_path(), real_path(cfg.complete_dir.get_path(), kwargs["dir"])): 2362 return T("Category folder cannot be a subfolder of the Temporary Download Folder.") 2363 2364 # Delete current one and replace with new one 2365 if name: 2366 config.delete("categories", name) 2367 config.ConfigCat(newname.lower(), kwargs) 2368 2369 config.save_config() 2370 raise Raiser(self.__root) 2371 2372 2373############################################################################## 2374SORT_LIST = ( 2375 "enable_tv_sorting", 2376 "tv_sort_string", 2377 "tv_categories", 2378 "enable_movie_sorting", 2379 "movie_sort_string", 2380 "movie_sort_extra", 2381 "movie_extra_folder", 2382 "enable_date_sorting", 2383 "date_sort_string", 2384 "movie_categories", 2385 "date_categories", 2386) 2387 2388 2389class ConfigSorting: 2390 def __init__(self, root): 2391 self.__root = root 2392 2393 @secured_expose(check_configlock=True) 2394 def index(self, **kwargs): 2395 conf = build_header(sabnzbd.WEB_DIR_CONFIG) 2396 conf["complete_dir"] = cfg.complete_dir.get_clipped_path() 2397 2398 for kw in SORT_LIST: 2399 conf[kw] = config.get_config("misc", kw)() 2400 conf["categories"] = list_cats(False) 2401 2402 template = Template( 2403 file=os.path.join(sabnzbd.WEB_DIR_CONFIG, "config_sorting.tmpl"), 2404 searchList=[conf], 2405 compilerSettings=CHEETAH_DIRECTIVES, 2406 ) 2407 return template.respond() 2408 2409 @secured_expose(check_api_key=True, check_configlock=True) 2410 def saveSorting(self, **kwargs): 2411 try: 2412 kwargs["movie_categories"] = kwargs["movie_cat"] 2413 except: 2414 pass 2415 try: 2416 kwargs["date_categories"] = kwargs["date_cat"] 2417 except: 2418 pass 2419 try: 2420 kwargs["tv_categories"] = kwargs["tv_cat"] 2421 except: 2422 pass 2423 2424 for kw in SORT_LIST: 2425 item = config.get_config("misc", kw) 2426 value = kwargs.get(kw) 2427 msg = item.set(value) 2428 if msg: 2429 return badParameterResponse(msg) 2430 2431 config.save_config() 2432 raise Raiser(self.__root) 2433 2434 2435############################################################################## 2436LOG_API_RE = re.compile(rb"(apikey|api)(=|:)[\w]+", re.I) 2437LOG_API_JSON_RE = re.compile(rb"'(apikey|api)': '[\w]+'", re.I) 2438LOG_USER_RE = re.compile(rb"(user|username)\s?=\s?[\S]+", re.I) 2439LOG_PASS_RE = re.compile(rb"(password)\s?=\s?[\S]+", re.I) 2440LOG_INI_HIDE_RE = re.compile( 2441 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]+", 2442 re.I, 2443) 2444LOG_HASH_RE = re.compile(rb"([a-fA-F\d]{25})", re.I) 2445 2446 2447class Status: 2448 def __init__(self, root): 2449 self.__root = root 2450 2451 @secured_expose(check_configlock=True) 2452 def index(self, **kwargs): 2453 header = build_status(skip_dashboard=kwargs.get("skip_dashboard")) 2454 template = Template( 2455 file=os.path.join(sabnzbd.WEB_DIR, "status.tmpl"), searchList=[header], compilerSettings=CHEETAH_DIRECTIVES 2456 ) 2457 return template.respond() 2458 2459 @secured_expose(check_api_key=True) 2460 def reset_quota(self, **kwargs): 2461 sabnzbd.BPSMeter.reset_quota(force=True) 2462 raise Raiser(self.__root) 2463 2464 @secured_expose(check_api_key=True) 2465 def disconnect(self, **kwargs): 2466 sabnzbd.Downloader.disconnect() 2467 raise Raiser(self.__root) 2468 2469 @secured_expose(check_api_key=True) 2470 def refresh_conn(self, **kwargs): 2471 # No real action, just reload the page 2472 raise Raiser(self.__root) 2473 2474 @secured_expose(check_api_key=True) 2475 def showlog(self, **kwargs): 2476 try: 2477 sabnzbd.LOGHANDLER.flush() 2478 except: 2479 pass 2480 2481 # Fetch the INI and the log-data and add a message at the top 2482 log_data = b"--------------------------------\n\n" 2483 log_data += b"The log includes a copy of your sabnzbd.ini with\nall usernames, passwords and API-keys removed." 2484 log_data += b"\n\n--------------------------------\n" 2485 2486 with open(sabnzbd.LOGFILE, "rb") as f: 2487 log_data += f.read() 2488 2489 with open(config.get_filename(), "rb") as f: 2490 log_data += f.read() 2491 2492 # We need to remove all passwords/usernames/api-keys 2493 log_data = LOG_API_RE.sub(b"apikey=<APIKEY>", log_data) 2494 log_data = LOG_API_JSON_RE.sub(b"'apikey':<APIKEY>'", log_data) 2495 log_data = LOG_USER_RE.sub(b"\\g<1>=<USER>", log_data) 2496 log_data = LOG_PASS_RE.sub(b"password=<PASSWORD>", log_data) 2497 log_data = LOG_INI_HIDE_RE.sub(b"\\1 = <REMOVED>", log_data) 2498 log_data = LOG_HASH_RE.sub(b"<HASH>", log_data) 2499 2500 # Try to replace the username 2501 try: 2502 import getpass 2503 2504 cur_user = getpass.getuser() 2505 if cur_user: 2506 log_data = log_data.replace(utob(cur_user), b"<USERNAME>") 2507 except: 2508 pass 2509 # Set headers 2510 cherrypy.response.headers["Content-Type"] = "application/x-download;charset=utf-8" 2511 cherrypy.response.headers["Content-Disposition"] = 'attachment;filename="sabnzbd.log"' 2512 return log_data 2513 2514 @secured_expose(check_api_key=True) 2515 def clearwarnings(self, **kwargs): 2516 sabnzbd.GUIHANDLER.clear() 2517 raise Raiser(self.__root) 2518 2519 @secured_expose(check_api_key=True) 2520 def change_loglevel(self, **kwargs): 2521 cfg.log_level.set(kwargs.get("loglevel")) 2522 config.save_config() 2523 2524 raise Raiser(self.__root) 2525 2526 @secured_expose(check_api_key=True) 2527 def unblock_server(self, **kwargs): 2528 sabnzbd.Downloader.unblock(kwargs.get("server")) 2529 # Short sleep so that UI shows new server status 2530 time.sleep(1.0) 2531 raise Raiser(self.__root) 2532 2533 @secured_expose(check_api_key=True) 2534 def delete(self, **kwargs): 2535 orphan_delete(kwargs) 2536 raise Raiser(self.__root) 2537 2538 @secured_expose(check_api_key=True) 2539 def delete_all(self, **kwargs): 2540 orphan_delete_all() 2541 raise Raiser(self.__root) 2542 2543 @secured_expose(check_api_key=True) 2544 def add(self, **kwargs): 2545 orphan_add(kwargs) 2546 raise Raiser(self.__root) 2547 2548 @secured_expose(check_api_key=True) 2549 def add_all(self, **kwargs): 2550 orphan_add_all() 2551 raise Raiser(self.__root) 2552 2553 @secured_expose(check_api_key=True) 2554 def dashrefresh(self, **kwargs): 2555 # This function is run when Refresh button on Dashboard is clicked 2556 # Put the time consuming dashboard functions here; they only get executed when the user clicks the Refresh button 2557 2558 # PyStone 2559 sabnzbd.PYSTONE_SCORE = getpystone() 2560 2561 # Diskspeed of download (aka incomplete) directory: 2562 dir_speed = diskspeedmeasure(sabnzbd.cfg.download_dir.get_path()) 2563 if dir_speed: 2564 sabnzbd.DOWNLOAD_DIR_SPEED = round(dir_speed, 1) 2565 else: 2566 sabnzbd.DOWNLOAD_DIR_SPEED = 0 2567 2568 time.sleep(1.0) 2569 # Diskspeed of complete directory: 2570 dir_speed = diskspeedmeasure(sabnzbd.cfg.complete_dir.get_path()) 2571 if dir_speed: 2572 sabnzbd.COMPLETE_DIR_SPEED = round(dir_speed, 1) 2573 else: 2574 sabnzbd.COMPLETE_DIR_SPEED = 0 2575 2576 # Internet bandwidth 2577 sabnzbd.INTERNET_BANDWIDTH = round(internetspeed(), 1) 2578 2579 raise Raiser(self.__root) # Refresh screen 2580 2581 2582def orphan_delete(kwargs): 2583 path = kwargs.get("name") 2584 if path: 2585 path = os.path.join(long_path(cfg.download_dir.get_path()), path) 2586 logging.info("Removing orphaned job %s", path) 2587 remove_all(path, recursive=True) 2588 2589 2590def orphan_delete_all(): 2591 paths = sabnzbd.NzbQueue.scan_jobs(all_jobs=False, action=False) 2592 for path in paths: 2593 kwargs = {"name": path} 2594 orphan_delete(kwargs) 2595 2596 2597def orphan_add(kwargs): 2598 path = kwargs.get("name") 2599 if path: 2600 path = os.path.join(long_path(cfg.download_dir.get_path()), path) 2601 logging.info("Re-adding orphaned job %s", path) 2602 sabnzbd.NzbQueue.repair_job(path, None, None) 2603 2604 2605def orphan_add_all(): 2606 paths = sabnzbd.NzbQueue.scan_jobs(all_jobs=False, action=False) 2607 for path in paths: 2608 kwargs = {"name": path} 2609 orphan_add(kwargs) 2610 2611 2612def badParameterResponse(msg, ajax=None): 2613 """Return a html page with error message and a 'back' button""" 2614 if ajax: 2615 return sabnzbd.api.report("json", error=msg) 2616 else: 2617 return """ 2618<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN"> 2619<html> 2620<head> 2621 <title>SABnzbd %s - %s</title> 2622</head> 2623<body> 2624<h3>%s</h3> 2625%s 2626<br><br> 2627<FORM><INPUT TYPE="BUTTON" VALUE="%s" ONCLICK="history.go(-1)"></FORM> 2628</body> 2629</html> 2630""" % ( 2631 sabnzbd.__version__, 2632 T("ERROR:"), 2633 T("Incorrect parameter"), 2634 msg, 2635 T("Back"), 2636 ) 2637 2638 2639def ShowString(name, msg): 2640 """Return a html page listing a file and a 'back' button""" 2641 return """ 2642<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN"> 2643<html> 2644<head> 2645 <title>%s</title> 2646</head> 2647<body> 2648 <FORM><INPUT TYPE="BUTTON" VALUE="%s" ONCLICK="history.go(-1)"></FORM> 2649 <h3>%s</h3> 2650 <code><pre>%s</pre></code> 2651</body> 2652</html> 2653""" % ( 2654 xml_name(name), 2655 T("Back"), 2656 xml_name(name), 2657 escape(msg), 2658 ) 2659 2660 2661def GetRssLog(feed): 2662 def make_item(job): 2663 # Make a copy 2664 job = job.copy() 2665 2666 # Now we apply some formatting 2667 job["title"] = job["title"] 2668 job["skip"] = "*" * int(job.get("status", "").endswith("*")) 2669 # These fields could be empty 2670 job["cat"] = job.get("cat", "") 2671 job["size"] = job.get("size", "") 2672 job["infourl"] = job.get("infourl", "") 2673 2674 # Auto-fetched jobs didn't have these fields set 2675 if job.get("url"): 2676 job["baselink"] = get_base_url(job.get("url")) 2677 if sabnzbd.rss.special_rss_site(job.get("url")): 2678 job["nzbname"] = "" 2679 else: 2680 job["nzbname"] = job["title"] 2681 else: 2682 job["baselink"] = "" 2683 job["nzbname"] = job["title"] 2684 2685 if job.get("size", 0): 2686 job["size_units"] = to_units(job["size"]) 2687 else: 2688 job["size_units"] = "-" 2689 2690 # And we add extra fields for sorting 2691 if job.get("age", 0): 2692 job["age_ms"] = (job["age"] - datetime.utcfromtimestamp(0)).total_seconds() 2693 job["age"] = calc_age(job["age"], True) 2694 else: 2695 job["age_ms"] = "" 2696 job["age"] = "" 2697 2698 if job.get("time_downloaded"): 2699 job["time_downloaded_ms"] = time.mktime(job["time_downloaded"]) 2700 job["time_downloaded"] = time.strftime(time_format("%H:%M %a %d %b"), job["time_downloaded"]) 2701 else: 2702 job["time_downloaded_ms"] = "" 2703 job["time_downloaded"] = "" 2704 2705 return job 2706 2707 jobs = sabnzbd.RSSReader.show_result(feed).values() 2708 good, bad, done = ([], [], []) 2709 for job in jobs: 2710 if job["status"][0] == "G": 2711 good.append(make_item(job)) 2712 elif job["status"][0] == "B": 2713 bad.append(make_item(job)) 2714 elif job["status"] == "D": 2715 done.append(make_item(job)) 2716 2717 try: 2718 # Sort based on actual age, in try-catch just to be sure 2719 good.sort(key=lambda job: job["age_ms"], reverse=True) 2720 bad.sort(key=lambda job: job["age_ms"], reverse=True) 2721 done.sort(key=lambda job: job["time_downloaded_ms"], reverse=True) 2722 except: 2723 # Let the javascript do it then.. 2724 pass 2725 2726 return done, good, bad 2727 2728 2729############################################################################## 2730LIST_EMAIL = ( 2731 "email_endjob", 2732 "email_cats", 2733 "email_full", 2734 "email_server", 2735 "email_to", 2736 "email_from", 2737 "email_account", 2738 "email_pwd", 2739 "email_rss", 2740) 2741LIST_NCENTER = ( 2742 "ncenter_enable", 2743 "ncenter_cats", 2744 "ncenter_prio_startup", 2745 "ncenter_prio_download", 2746 "ncenter_prio_pause_resume", 2747 "ncenter_prio_pp", 2748 "ncenter_prio_pp", 2749 "ncenter_prio_complete", 2750 "ncenter_prio_failed", 2751 "ncenter_prio_disk_full", 2752 "ncenter_prio_warning", 2753 "ncenter_prio_error", 2754 "ncenter_prio_queue_done", 2755 "ncenter_prio_other", 2756 "ncenter_prio_new_login", 2757) 2758LIST_ACENTER = ( 2759 "acenter_enable", 2760 "acenter_cats", 2761 "acenter_prio_startup", 2762 "acenter_prio_download", 2763 "acenter_prio_pause_resume", 2764 "acenter_prio_pp", 2765 "acenter_prio_complete", 2766 "acenter_prio_failed", 2767 "acenter_prio_disk_full", 2768 "acenter_prio_warning", 2769 "acenter_prio_error", 2770 "acenter_prio_queue_done", 2771 "acenter_prio_other", 2772 "acenter_prio_new_login", 2773) 2774LIST_NTFOSD = ( 2775 "ntfosd_enable", 2776 "ntfosd_cats", 2777 "ntfosd_prio_startup", 2778 "ntfosd_prio_download", 2779 "ntfosd_prio_pause_resume", 2780 "ntfosd_prio_pp", 2781 "ntfosd_prio_complete", 2782 "ntfosd_prio_failed", 2783 "ntfosd_prio_disk_full", 2784 "ntfosd_prio_warning", 2785 "ntfosd_prio_error", 2786 "ntfosd_prio_queue_done", 2787 "ntfosd_prio_other", 2788 "ntfosd_prio_new_login", 2789) 2790LIST_PROWL = ( 2791 "prowl_enable", 2792 "prowl_cats", 2793 "prowl_apikey", 2794 "prowl_prio_startup", 2795 "prowl_prio_download", 2796 "prowl_prio_pause_resume", 2797 "prowl_prio_pp", 2798 "prowl_prio_complete", 2799 "prowl_prio_failed", 2800 "prowl_prio_disk_full", 2801 "prowl_prio_warning", 2802 "prowl_prio_error", 2803 "prowl_prio_queue_done", 2804 "prowl_prio_other", 2805 "prowl_prio_new_login", 2806) 2807LIST_PUSHOVER = ( 2808 "pushover_enable", 2809 "pushover_cats", 2810 "pushover_token", 2811 "pushover_userkey", 2812 "pushover_device", 2813 "pushover_prio_startup", 2814 "pushover_prio_download", 2815 "pushover_prio_pause_resume", 2816 "pushover_prio_pp", 2817 "pushover_prio_complete", 2818 "pushover_prio_failed", 2819 "pushover_prio_disk_full", 2820 "pushover_prio_warning", 2821 "pushover_prio_error", 2822 "pushover_prio_queue_done", 2823 "pushover_prio_other", 2824 "pushover_prio_new_login", 2825 "pushover_emergency_retry", 2826 "pushover_emergency_expire", 2827) 2828LIST_PUSHBULLET = ( 2829 "pushbullet_enable", 2830 "pushbullet_cats", 2831 "pushbullet_apikey", 2832 "pushbullet_device", 2833 "pushbullet_prio_startup", 2834 "pushbullet_prio_download", 2835 "pushbullet_prio_pause_resume", 2836 "pushbullet_prio_pp", 2837 "pushbullet_prio_complete", 2838 "pushbullet_prio_failed", 2839 "pushbullet_prio_disk_full", 2840 "pushbullet_prio_warning", 2841 "pushbullet_prio_error", 2842 "pushbullet_prio_queue_done", 2843 "pushbullet_prio_other", 2844 "pushbullet_prio_new_login", 2845) 2846LIST_NSCRIPT = ( 2847 "nscript_enable", 2848 "nscript_cats", 2849 "nscript_script", 2850 "nscript_parameters", 2851 "nscript_prio_startup", 2852 "nscript_prio_download", 2853 "nscript_prio_pause_resume", 2854 "nscript_prio_pp", 2855 "nscript_prio_complete", 2856 "nscript_prio_failed", 2857 "nscript_prio_disk_full", 2858 "nscript_prio_warning", 2859 "nscript_prio_error", 2860 "nscript_prio_queue_done", 2861 "nscript_prio_other", 2862 "nscript_prio_new_login", 2863) 2864 2865 2866class ConfigNotify: 2867 def __init__(self, root): 2868 self.__root = root 2869 self.__lastmail = None 2870 2871 @secured_expose(check_configlock=True) 2872 def index(self, **kwargs): 2873 conf = build_header(sabnzbd.WEB_DIR_CONFIG) 2874 2875 conf["categories"] = list_cats(False) 2876 conf["lastmail"] = self.__lastmail 2877 conf["have_ntfosd"] = sabnzbd.notifier.have_ntfosd() 2878 conf["have_ncenter"] = sabnzbd.DARWIN and sabnzbd.FOUNDATION 2879 conf["scripts"] = list_scripts(default=False, none=True) 2880 2881 for kw in LIST_EMAIL: 2882 conf[kw] = config.get_config("misc", kw).get_string() 2883 for kw in LIST_PROWL: 2884 conf[kw] = config.get_config("prowl", kw)() 2885 for kw in LIST_PUSHOVER: 2886 conf[kw] = config.get_config("pushover", kw)() 2887 for kw in LIST_PUSHBULLET: 2888 conf[kw] = config.get_config("pushbullet", kw)() 2889 for kw in LIST_NCENTER: 2890 conf[kw] = config.get_config("ncenter", kw)() 2891 for kw in LIST_ACENTER: 2892 conf[kw] = config.get_config("acenter", kw)() 2893 for kw in LIST_NTFOSD: 2894 conf[kw] = config.get_config("ntfosd", kw)() 2895 for kw in LIST_NSCRIPT: 2896 conf[kw] = config.get_config("nscript", kw)() 2897 conf["notify_types"] = sabnzbd.notifier.NOTIFICATION 2898 2899 template = Template( 2900 file=os.path.join(sabnzbd.WEB_DIR_CONFIG, "config_notify.tmpl"), 2901 searchList=[conf], 2902 compilerSettings=CHEETAH_DIRECTIVES, 2903 ) 2904 return template.respond() 2905 2906 @secured_expose(check_api_key=True, check_configlock=True) 2907 def saveEmail(self, **kwargs): 2908 ajax = kwargs.get("ajax") 2909 2910 for kw in LIST_EMAIL: 2911 msg = config.get_config("misc", kw).set(kwargs.get(kw)) 2912 if msg: 2913 return badParameterResponse(T("Incorrect value for %s: %s") % (kw, msg), ajax) 2914 for kw in LIST_NCENTER: 2915 msg = config.get_config("ncenter", kw).set(kwargs.get(kw)) 2916 if msg: 2917 return badParameterResponse(T("Incorrect value for %s: %s") % (kw, msg), ajax) 2918 for kw in LIST_ACENTER: 2919 msg = config.get_config("acenter", kw).set(kwargs.get(kw)) 2920 if msg: 2921 return badParameterResponse(T("Incorrect value for %s: %s") % (kw, msg), ajax) 2922 for kw in LIST_NTFOSD: 2923 msg = config.get_config("ntfosd", kw).set(kwargs.get(kw)) 2924 if msg: 2925 return badParameterResponse(T("Incorrect value for %s: %s") % (kw, msg), ajax) 2926 for kw in LIST_PROWL: 2927 msg = config.get_config("prowl", kw).set(kwargs.get(kw)) 2928 if msg: 2929 return badParameterResponse(T("Incorrect value for %s: %s") % (kw, msg), ajax) 2930 for kw in LIST_PUSHOVER: 2931 msg = config.get_config("pushover", kw).set(kwargs.get(kw)) 2932 if msg: 2933 return badParameterResponse(T("Incorrect value for %s: %s") % (kw, msg), ajax) 2934 for kw in LIST_PUSHBULLET: 2935 msg = config.get_config("pushbullet", kw).set(kwargs.get(kw, 0)) 2936 if msg: 2937 return badParameterResponse(T("Incorrect value for %s: %s") % (kw, msg), ajax) 2938 for kw in LIST_NSCRIPT: 2939 msg = config.get_config("nscript", kw).set(kwargs.get(kw, 0)) 2940 if msg: 2941 return badParameterResponse(T("Incorrect value for %s: %s") % (kw, msg), ajax) 2942 2943 config.save_config() 2944 self.__lastmail = None 2945 if ajax: 2946 return sabnzbd.api.report("json") 2947 else: 2948 raise Raiser(self.__root) 2949