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.api - api 20""" 21 22import os 23import logging 24import re 25import gc 26import datetime 27import time 28import json 29import cherrypy 30import locale 31from threading import Thread 32from typing import Tuple, Optional, List 33 34import sabnzbd 35from sabnzbd.constants import ( 36 VALID_ARCHIVES, 37 VALID_NZB_FILES, 38 Status, 39 FORCE_PRIORITY, 40 NORMAL_PRIORITY, 41 INTERFACE_PRIORITIES, 42 KIBI, 43 MEBI, 44 GIGI, 45) 46import sabnzbd.config as config 47import sabnzbd.cfg as cfg 48from sabnzbd.skintext import SKIN_TEXT 49from sabnzbd.utils.pathbrowser import folders_at_path 50from sabnzbd.utils.getperformance import getcpu 51from sabnzbd.misc import ( 52 loadavg, 53 to_units, 54 int_conv, 55 time_format, 56 cat_convert, 57 create_https_certificates, 58 calc_age, 59 opts_to_pp, 60) 61from sabnzbd.filesystem import diskspace, get_ext, clip_path, remove_all, list_scripts 62from sabnzbd.encoding import xml_name, utob 63from sabnzbd.utils.servertests import test_nntp_server_dict 64from sabnzbd.getipaddress import localipv4, publicipv4, ipv6, addresslookup 65from sabnzbd.database import build_history_info, unpack_history_info, HistoryDB 66from sabnzbd.lang import is_rtl 67import sabnzbd.notifier 68import sabnzbd.rss 69import sabnzbd.emailer 70import sabnzbd.sorting 71 72############################################################################## 73# API error messages 74############################################################################## 75_MSG_NO_VALUE = "expects one parameter" 76_MSG_NO_VALUE2 = "expects two parameters" 77_MSG_INT_VALUE = "expects integer value" 78_MSG_NO_ITEM = "item does not exist" 79_MSG_NOT_IMPLEMENTED = "not implemented" 80_MSG_NO_FILE = "no file given" 81_MSG_NO_PATH = "file does not exist" 82_MSG_OUTPUT_FORMAT = "Format not supported" 83_MSG_NO_SUCH_CONFIG = "Config item does not exist" 84_MSG_CONFIG_LOCKED = "Configuration locked" 85_MSG_BAD_SERVER_PARMS = "Incorrect server settings" 86 87 88def api_handler(kwargs): 89 """API Dispatcher""" 90 # Clean-up the arguments 91 for vr in ("mode", "output", "name"): 92 if vr in kwargs and isinstance(kwargs[vr], list): 93 kwargs[vr] = kwargs[vr][0] 94 95 mode = kwargs.get("mode", "") 96 output = kwargs.get("output", "") 97 name = kwargs.get("name", "") 98 99 response = _api_table.get(mode, (_api_undefined, 2))[0](name, output, kwargs) 100 return response 101 102 103def _api_get_config(name, output, kwargs): 104 """API: accepts output, keyword, section""" 105 _, data = config.get_dconfig(kwargs.get("section"), kwargs.get("keyword")) 106 return report(output, keyword="config", data=data) 107 108 109def _api_set_config(name, output, kwargs): 110 """API: accepts output, keyword, section""" 111 if cfg.configlock(): 112 return report(output, _MSG_CONFIG_LOCKED) 113 if kwargs.get("section") == "servers": 114 kwargs["keyword"] = handle_server_api(output, kwargs) 115 elif kwargs.get("section") == "rss": 116 kwargs["keyword"] = handle_rss_api(output, kwargs) 117 elif kwargs.get("section") == "categories": 118 kwargs["keyword"] = handle_cat_api(output, kwargs) 119 else: 120 res = config.set_config(kwargs) 121 if not res: 122 return report(output, _MSG_NO_SUCH_CONFIG) 123 config.save_config() 124 res, data = config.get_dconfig(kwargs.get("section"), kwargs.get("keyword")) 125 return report(output, keyword="config", data=data) 126 127 128def _api_set_config_default(name, output, kwargs): 129 """API: Reset requested config variables back to defaults. Currently only for misc-section""" 130 if cfg.configlock(): 131 return report(output, _MSG_CONFIG_LOCKED) 132 keywords = kwargs.get("keyword", []) 133 if not isinstance(keywords, list): 134 keywords = [keywords] 135 for keyword in keywords: 136 item = config.get_config("misc", keyword) 137 if item: 138 item.set(item.default()) 139 config.save_config() 140 return report(output) 141 142 143def _api_del_config(name, output, kwargs): 144 """API: accepts output, keyword, section""" 145 if cfg.configlock(): 146 return report(output, _MSG_CONFIG_LOCKED) 147 if del_from_section(kwargs): 148 return report(output) 149 else: 150 return report(output, _MSG_NOT_IMPLEMENTED) 151 152 153def _api_queue(name, output, kwargs): 154 """API: Dispatcher for mode=queue""" 155 value = kwargs.get("value", "") 156 return _api_queue_table.get(name, (_api_queue_default, 2))[0](output, value, kwargs) 157 158 159def _api_queue_delete(output, value, kwargs): 160 """API: accepts output, value""" 161 if value.lower() == "all": 162 removed = sabnzbd.NzbQueue.remove_all(kwargs.get("search")) 163 return report(output, keyword="", data={"status": bool(removed), "nzo_ids": removed}) 164 elif value: 165 items = value.split(",") 166 delete_all_data = int_conv(kwargs.get("del_files")) 167 removed = sabnzbd.NzbQueue.remove_multiple(items, delete_all_data=delete_all_data) 168 return report(output, keyword="", data={"status": bool(removed), "nzo_ids": removed}) 169 else: 170 return report(output, _MSG_NO_VALUE) 171 172 173def _api_queue_delete_nzf(output, value, kwargs): 174 """API: accepts value(=nzo_id), value2(=nzf_id)""" 175 value2 = kwargs.get("value2") 176 if value and value2: 177 removed = sabnzbd.NzbQueue.remove_nzf(value, value2, force_delete=True) 178 return report(output, keyword="", data={"status": bool(removed), "nzf_ids": removed}) 179 else: 180 return report(output, _MSG_NO_VALUE2) 181 182 183def _api_queue_rename(output, value, kwargs): 184 """API: accepts output, value(=old name), value2(=new name), value3(=password)""" 185 value2 = kwargs.get("value2") 186 value3 = kwargs.get("value3") 187 if value and value2: 188 ret = sabnzbd.NzbQueue.change_name(value, value2, value3) 189 return report(output, keyword="", data={"status": ret}) 190 else: 191 return report(output, _MSG_NO_VALUE2) 192 193 194def _api_queue_change_complete_action(output, value, kwargs): 195 """API: accepts output, value(=action)""" 196 sabnzbd.change_queue_complete_action(value) 197 return report(output) 198 199 200def _api_queue_purge(output, value, kwargs): 201 """API: accepts output""" 202 removed = sabnzbd.NzbQueue.remove_all(kwargs.get("search")) 203 return report(output, keyword="", data={"status": bool(removed), "nzo_ids": removed}) 204 205 206def _api_queue_pause(output, value, kwargs): 207 """API: accepts output, value(=list of nzo_id)""" 208 if value: 209 items = value.split(",") 210 handled = sabnzbd.NzbQueue.pause_multiple_nzo(items) 211 else: 212 handled = False 213 return report(output, keyword="", data={"status": bool(handled), "nzo_ids": handled}) 214 215 216def _api_queue_resume(output, value, kwargs): 217 """API: accepts output, value(=list of nzo_id)""" 218 if value: 219 items = value.split(",") 220 handled = sabnzbd.NzbQueue.resume_multiple_nzo(items) 221 else: 222 handled = False 223 return report(output, keyword="", data={"status": bool(handled), "nzo_ids": handled}) 224 225 226def _api_queue_priority(output, value, kwargs): 227 """API: accepts output, value(=nzo_id), value2(=priority)""" 228 value2 = kwargs.get("value2") 229 if value and value2: 230 try: 231 try: 232 priority = int(value2) 233 except: 234 return report(output, _MSG_INT_VALUE) 235 pos = sabnzbd.NzbQueue.set_priority(value, priority) 236 # Returns the position in the queue, -1 is incorrect job-id 237 return report(output, keyword="position", data=pos) 238 except: 239 return report(output, _MSG_NO_VALUE2) 240 else: 241 return report(output, _MSG_NO_VALUE2) 242 243 244def _api_queue_sort(output, value, kwargs): 245 """API: accepts output, sort, dir""" 246 sort = kwargs.get("sort") 247 direction = kwargs.get("dir", "") 248 if sort: 249 sabnzbd.NzbQueue.sort_queue(sort, direction) 250 return report(output) 251 else: 252 return report(output, _MSG_NO_VALUE2) 253 254 255def _api_queue_default(output, value, kwargs): 256 """API: accepts output, sort, dir, start, limit""" 257 start = int_conv(kwargs.get("start")) 258 limit = int_conv(kwargs.get("limit")) 259 search = kwargs.get("search") 260 nzo_ids = kwargs.get("nzo_ids") 261 262 info, pnfo_list, bytespersec = build_queue(start=start, limit=limit, output=output, search=search, nzo_ids=nzo_ids) 263 return report(output, keyword="queue", data=info) 264 265 266def _api_queue_rating(output, value, kwargs): 267 """API: accepts output, value(=nzo_id), type, setting, detail""" 268 vote_map = {"up": sabnzbd.Rating.VOTE_UP, "down": sabnzbd.Rating.VOTE_DOWN} 269 flag_map = { 270 "spam": sabnzbd.Rating.FLAG_SPAM, 271 "encrypted": sabnzbd.Rating.FLAG_ENCRYPTED, 272 "expired": sabnzbd.Rating.FLAG_EXPIRED, 273 "other": sabnzbd.Rating.FLAG_OTHER, 274 "comment": sabnzbd.Rating.FLAG_COMMENT, 275 } 276 content_type = kwargs.get("type") 277 setting = kwargs.get("setting") 278 if value: 279 try: 280 video = audio = vote = flag = None 281 if content_type == "video" and setting != "-": 282 video = setting 283 if content_type == "audio" and setting != "-": 284 audio = setting 285 if content_type == "vote": 286 vote = vote_map[setting] 287 if content_type == "flag": 288 flag = flag_map[setting] 289 if cfg.rating_enable(): 290 sabnzbd.Rating.update_user_rating(value, video, audio, vote, flag, kwargs.get("detail")) 291 return report(output) 292 except: 293 return report(output, _MSG_BAD_SERVER_PARMS) 294 else: 295 return report(output, _MSG_NO_VALUE) 296 297 298def _api_options(name, output, kwargs): 299 """API: accepts output""" 300 return options_list(output) 301 302 303def _api_translate(name, output, kwargs): 304 """API: accepts output, value(=acronym)""" 305 return report(output, keyword="value", data=T(kwargs.get("value", ""))) 306 307 308def _api_addfile(name, output, kwargs): 309 """API: accepts name, output, pp, script, cat, priority, nzbname""" 310 # Normal upload will send the nzb in a kw arg called name or nzbfile 311 if not name or isinstance(name, str): 312 name = kwargs.get("nzbfile", None) 313 if hasattr(name, "file") and hasattr(name, "filename") and name.filename: 314 cat = kwargs.get("cat") 315 xcat = kwargs.get("xcat") 316 if not cat and xcat: 317 # Indexer category, so do mapping 318 cat = cat_convert(xcat) 319 # Add the NZB-file 320 res, nzo_ids = sabnzbd.add_nzbfile( 321 name, 322 pp=kwargs.get("pp"), 323 script=kwargs.get("script"), 324 cat=cat, 325 priority=kwargs.get("priority"), 326 nzbname=kwargs.get("nzbname"), 327 password=kwargs.get("password"), 328 ) 329 return report(output, keyword="", data={"status": res == 0, "nzo_ids": nzo_ids}) 330 else: 331 return report(output, _MSG_NO_VALUE) 332 333 334def _api_retry(name, output, kwargs): 335 """API: accepts name, output, value(=nzo_id), nzbfile(=optional NZB), password (optional)""" 336 value = kwargs.get("value") 337 # Normal upload will send the nzb in a kw arg called nzbfile 338 if name is None or isinstance(name, str): 339 name = kwargs.get("nzbfile") 340 password = kwargs.get("password") 341 password = password[0] if isinstance(password, list) else password 342 343 nzo_id = retry_job(value, name, password) 344 if nzo_id: 345 return report(output, keyword="", data={"status": True, "nzo_id": nzo_id}) 346 else: 347 return report(output, _MSG_NO_ITEM) 348 349 350def _api_cancel_pp(name, output, kwargs): 351 """API: accepts name, output, value(=nzo_id)""" 352 nzo_id = kwargs.get("value") 353 if sabnzbd.PostProcessor.cancel_pp(nzo_id): 354 return report(output, keyword="", data={"status": True, "nzo_id": nzo_id}) 355 else: 356 return report(output, _MSG_NO_ITEM) 357 358 359def _api_addlocalfile(name, output, kwargs): 360 """API: accepts name, output, pp, script, cat, priority, nzbname""" 361 if name: 362 if os.path.exists(name): 363 pp = kwargs.get("pp") 364 script = kwargs.get("script") 365 cat = kwargs.get("cat") 366 xcat = kwargs.get("xcat") 367 if not cat and xcat: 368 # Indexer category, so do mapping 369 cat = cat_convert(xcat) 370 priority = kwargs.get("priority") 371 nzbname = kwargs.get("nzbname") 372 password = kwargs.get("password") 373 374 if get_ext(name) in VALID_ARCHIVES + VALID_NZB_FILES: 375 res, nzo_ids = sabnzbd.add_nzbfile( 376 name, 377 pp=pp, 378 script=script, 379 cat=cat, 380 priority=priority, 381 keep=True, 382 nzbname=nzbname, 383 password=password, 384 ) 385 return report(output, keyword="", data={"status": res == 0, "nzo_ids": nzo_ids}) 386 else: 387 logging.info('API-call addlocalfile: "%s" is not a supported file', name) 388 return report(output, _MSG_NO_FILE) 389 else: 390 logging.info('API-call addlocalfile: file "%s" not found', name) 391 return report(output, _MSG_NO_PATH) 392 else: 393 logging.info("API-call addlocalfile: no file name given") 394 return report(output, _MSG_NO_VALUE) 395 396 397def _api_switch(name, output, kwargs): 398 """API: accepts output, value(=first id), value2(=second id)""" 399 value = kwargs.get("value") 400 value2 = kwargs.get("value2") 401 if value and value2: 402 pos, prio = sabnzbd.NzbQueue.switch(value, value2) 403 # Returns the new position and new priority (if different) 404 return report(output, keyword="result", data={"position": pos, "priority": prio}) 405 else: 406 return report(output, _MSG_NO_VALUE2) 407 408 409def _api_change_cat(name, output, kwargs): 410 """API: accepts output, value(=nzo_id), value2(=category)""" 411 value = kwargs.get("value") 412 value2 = kwargs.get("value2") 413 if value and value2: 414 nzo_id = value 415 cat = value2 416 if cat == "None": 417 cat = None 418 result = sabnzbd.NzbQueue.change_cat(nzo_id, cat) 419 return report(output, keyword="status", data=bool(result > 0)) 420 else: 421 return report(output, _MSG_NO_VALUE) 422 423 424def _api_change_script(name, output, kwargs): 425 """API: accepts output, value(=nzo_id), value2(=script)""" 426 value = kwargs.get("value") 427 value2 = kwargs.get("value2") 428 if value and value2: 429 nzo_id = value 430 script = value2 431 if script.lower() == "none": 432 script = None 433 result = sabnzbd.NzbQueue.change_script(nzo_id, script) 434 return report(output, keyword="status", data=bool(result > 0)) 435 else: 436 return report(output, _MSG_NO_VALUE) 437 438 439def _api_change_opts(name, output, kwargs): 440 """API: accepts output, value(=nzo_id), value2(=pp)""" 441 value = kwargs.get("value") 442 value2 = kwargs.get("value2") 443 result = 0 444 if value and value2 and value2.isdigit(): 445 result = sabnzbd.NzbQueue.change_opts(value, int(value2)) 446 return report(output, keyword="status", data=bool(result > 0)) 447 448 449def _api_fullstatus(name, output, kwargs): 450 """API: full history status""" 451 status = build_status(skip_dashboard=kwargs.get("skip_dashboard", 1), output=output) 452 return report(output, keyword="status", data=status) 453 454 455def _api_history(name, output, kwargs): 456 """API: accepts output, value(=nzo_id), start, limit, search, nzo_ids""" 457 value = kwargs.get("value", "") 458 start = int_conv(kwargs.get("start")) 459 limit = int_conv(kwargs.get("limit")) 460 last_history_update = int_conv(kwargs.get("last_history_update", 0)) 461 search = kwargs.get("search") 462 failed_only = int_conv(kwargs.get("failed_only")) 463 categories = kwargs.get("category") 464 nzo_ids = kwargs.get("nzo_ids") 465 466 # Do we need to send anything? 467 if last_history_update == sabnzbd.LAST_HISTORY_UPDATE: 468 return report(output, keyword="history", data=False) 469 470 if categories and not isinstance(categories, list): 471 categories = [categories] 472 473 if nzo_ids and not isinstance(nzo_ids, list): 474 nzo_ids = nzo_ids.split(",") 475 476 if not limit: 477 limit = cfg.history_limit() 478 479 if name == "delete": 480 special = value.lower() 481 del_files = bool(int_conv(kwargs.get("del_files"))) 482 if special in ("all", "failed", "completed"): 483 history_db = sabnzbd.get_db_connection() 484 if special in ("all", "failed"): 485 if del_files: 486 del_job_files(history_db.get_failed_paths(search)) 487 history_db.remove_failed(search) 488 if special in ("all", "completed"): 489 history_db.remove_completed(search) 490 sabnzbd.history_updated() 491 return report(output) 492 elif value: 493 jobs = value.split(",") 494 for job in jobs: 495 del_hist_job(job, del_files) 496 sabnzbd.history_updated() 497 return report(output) 498 else: 499 return report(output, _MSG_NO_VALUE) 500 elif not name: 501 history = {} 502 grand, month, week, day = sabnzbd.BPSMeter.get_sums() 503 history["total_size"], history["month_size"], history["week_size"], history["day_size"] = ( 504 to_units(grand), 505 to_units(month), 506 to_units(week), 507 to_units(day), 508 ) 509 history["slots"], fetched_items, history["noofslots"] = build_history( 510 start=start, limit=limit, search=search, failed_only=failed_only, categories=categories, nzo_ids=nzo_ids 511 ) 512 history["last_history_update"] = sabnzbd.LAST_HISTORY_UPDATE 513 history["version"] = sabnzbd.__version__ 514 return report(output, keyword="history", data=history) 515 else: 516 return report(output, _MSG_NOT_IMPLEMENTED) 517 518 519def _api_get_files(name, output, kwargs): 520 """API: accepts output, value(=nzo_id)""" 521 value = kwargs.get("value") 522 if value: 523 return report(output, keyword="files", data=build_file_list(value)) 524 else: 525 return report(output, _MSG_NO_VALUE) 526 527 528def _api_addurl(name, output, kwargs): 529 """API: accepts name, output, pp, script, cat, priority, nzbname""" 530 pp = kwargs.get("pp") 531 script = kwargs.get("script") 532 cat = kwargs.get("cat") 533 priority = kwargs.get("priority") 534 nzbname = kwargs.get("nzbname", "") 535 password = kwargs.get("password", "") 536 537 if name: 538 nzo_id = sabnzbd.add_url(name, pp, script, cat, priority, nzbname, password) 539 # Reporting a list of NZO's, for compatibility with other add-methods 540 return report(output, keyword="", data={"status": True, "nzo_ids": [nzo_id]}) 541 else: 542 logging.info("API-call addurl: no URLs recieved") 543 return report(output, _MSG_NO_VALUE) 544 545 546def _api_pause(name, output, kwargs): 547 """API: accepts output""" 548 sabnzbd.Scheduler.plan_resume(0) 549 sabnzbd.Downloader.pause() 550 return report(output) 551 552 553def _api_resume(name, output, kwargs): 554 """API: accepts output""" 555 sabnzbd.Scheduler.plan_resume(0) 556 sabnzbd.unpause_all() 557 return report(output) 558 559 560def _api_shutdown(name, output, kwargs): 561 """API: accepts output""" 562 sabnzbd.shutdown_program() 563 return report(output) 564 565 566def _api_warnings(name, output, kwargs): 567 """API: accepts name, output""" 568 if name == "clear": 569 return report(output, keyword="warnings", data=sabnzbd.GUIHANDLER.clear()) 570 elif name == "show": 571 return report(output, keyword="warnings", data=sabnzbd.GUIHANDLER.content()) 572 elif name: 573 return report(output, _MSG_NOT_IMPLEMENTED) 574 return report(output, keyword="warnings", data=sabnzbd.GUIHANDLER.content()) 575 576 577def _api_get_cats(name, output, kwargs): 578 """API: accepts output""" 579 return report(output, keyword="categories", data=list_cats(False)) 580 581 582def _api_get_scripts(name, output, kwargs): 583 """API: accepts output""" 584 return report(output, keyword="scripts", data=list_scripts()) 585 586 587def _api_version(name, output, kwargs): 588 """API: accepts output""" 589 return report(output, keyword="version", data=sabnzbd.__version__) 590 591 592def _api_auth(name, output, kwargs): 593 """API: accepts output""" 594 auth = "None" 595 if not cfg.disable_key(): 596 auth = "badkey" 597 key = kwargs.get("key", "") 598 if not key: 599 auth = "apikey" 600 else: 601 if key == cfg.nzb_key(): 602 auth = "nzbkey" 603 if key == cfg.api_key(): 604 auth = "apikey" 605 elif cfg.username() and cfg.password(): 606 auth = "login" 607 return report(output, keyword="auth", data=auth) 608 609 610def _api_restart(name, output, kwargs): 611 """API: accepts output""" 612 logging.info("Restart requested by API") 613 # Do the shutdown async to still send goodbye to browser 614 Thread(target=sabnzbd.trigger_restart, kwargs={"timeout": 1}).start() 615 return report(output) 616 617 618def _api_restart_repair(name, output, kwargs): 619 """API: accepts output""" 620 logging.info("Queue repair requested by API") 621 sabnzbd.request_repair() 622 # Do the shutdown async to still send goodbye to browser 623 Thread(target=sabnzbd.trigger_restart, kwargs={"timeout": 1}).start() 624 return report(output) 625 626 627def _api_disconnect(name, output, kwargs): 628 """API: accepts output""" 629 sabnzbd.Downloader.disconnect() 630 return report(output) 631 632 633def _api_osx_icon(name, output, kwargs): 634 """API: accepts output, value""" 635 value = kwargs.get("value", "1").strip() 636 cfg.osx_menu.set(value != "0") 637 return report(output) 638 639 640def _api_rescan(name, output, kwargs): 641 """API: accepts output""" 642 sabnzbd.NzbQueue.scan_jobs(all_jobs=False, action=True) 643 return report(output) 644 645 646def _api_eval_sort(name, output, kwargs): 647 """API: evaluate sorting expression""" 648 name = kwargs.get("name", "") 649 value = kwargs.get("value", "") 650 title = kwargs.get("title") 651 multipart = kwargs.get("movieextra", "") 652 path = sabnzbd.sorting.eval_sort(value, title, name, multipart) 653 if path is None: 654 return report(output, _MSG_NOT_IMPLEMENTED) 655 else: 656 return report(output, keyword="result", data=path) 657 658 659def _api_watched_now(name, output, kwargs): 660 """API: accepts output""" 661 sabnzbd.DirScanner.scan() 662 return report(output) 663 664 665def _api_resume_pp(name, output, kwargs): 666 """API: accepts output""" 667 sabnzbd.PostProcessor.paused = False 668 return report(output) 669 670 671def _api_pause_pp(name, output, kwargs): 672 """API: accepts output""" 673 sabnzbd.PostProcessor.paused = True 674 return report(output) 675 676 677def _api_rss_now(name, output, kwargs): 678 """API: accepts output""" 679 # Run RSS scan async, because it can take a long time 680 sabnzbd.Scheduler.force_rss() 681 return report(output) 682 683 684def _api_retry_all(name, output, kwargs): 685 """API: Retry all failed items in History""" 686 return report(output, keyword="status", data=retry_all_jobs()) 687 688 689def _api_reset_quota(name, output, kwargs): 690 """Reset quota left""" 691 sabnzbd.BPSMeter.reset_quota(force=True) 692 return report(output) 693 694 695def _api_test_email(name, output, kwargs): 696 """API: send a test email, return result""" 697 logging.info("Sending test email") 698 pack = {"download": ["action 1", "action 2"], "unpack": ["action 1", "action 2"]} 699 res = sabnzbd.emailer.endjob( 700 "I had a d\xe8ja vu", 701 "unknown", 702 True, 703 os.path.normpath(os.path.join(cfg.complete_dir.get_path(), "/unknown/I had a d\xe8ja vu")), 704 123 * MEBI, 705 None, 706 pack, 707 "my_script", 708 "Line 1\nLine 2\nLine 3\nd\xe8ja vu\n", 709 0, 710 test=kwargs, 711 ) 712 if res == T("Email succeeded"): 713 return report(output) 714 return report(output, error=res) 715 716 717def _api_test_windows(name, output, kwargs): 718 """API: send a test to Windows, return result""" 719 logging.info("Sending test notification") 720 res = sabnzbd.notifier.send_windows("SABnzbd", T("Test Notification"), "other") 721 return report(output, error=res) 722 723 724def _api_test_notif(name, output, kwargs): 725 """API: send a test to Notification Center, return result""" 726 logging.info("Sending test notification") 727 res = sabnzbd.notifier.send_notification_center("SABnzbd", T("Test Notification"), "other") 728 return report(output, error=res) 729 730 731def _api_test_osd(name, output, kwargs): 732 """API: send a test OSD notification, return result""" 733 logging.info("Sending OSD notification") 734 res = sabnzbd.notifier.send_notify_osd("SABnzbd", T("Test Notification")) 735 return report(output, error=res) 736 737 738def _api_test_prowl(name, output, kwargs): 739 """API: send a test Prowl notification, return result""" 740 logging.info("Sending Prowl notification") 741 res = sabnzbd.notifier.send_prowl("SABnzbd", T("Test Notification"), "other", force=True, test=kwargs) 742 return report(output, error=res) 743 744 745def _api_test_pushover(name, output, kwargs): 746 """API: send a test Pushover notification, return result""" 747 logging.info("Sending Pushover notification") 748 res = sabnzbd.notifier.send_pushover("SABnzbd", T("Test Notification"), "other", force=True, test=kwargs) 749 return report(output, error=res) 750 751 752def _api_test_pushbullet(name, output, kwargs): 753 """API: send a test Pushbullet notification, return result""" 754 logging.info("Sending Pushbullet notification") 755 res = sabnzbd.notifier.send_pushbullet("SABnzbd", T("Test Notification"), "other", force=True, test=kwargs) 756 return report(output, error=res) 757 758 759def _api_test_nscript(name, output, kwargs): 760 """API: execute a test notification script, return result""" 761 logging.info("Executing notification script") 762 res = sabnzbd.notifier.send_nscript("SABnzbd", T("Test Notification"), "other", force=True, test=kwargs) 763 return report(output, error=res) 764 765 766def _api_undefined(name, output, kwargs): 767 """API: accepts output""" 768 return report(output, _MSG_NOT_IMPLEMENTED) 769 770 771def _api_browse(name, output, kwargs): 772 """Return tree of local path""" 773 compact = kwargs.get("compact") 774 775 if compact and compact == "1": 776 name = kwargs.get("term", "") 777 paths = [entry["path"] for entry in folders_at_path(os.path.dirname(name)) if "path" in entry] 778 return report(output, keyword="", data=paths) 779 else: 780 show_hidden = kwargs.get("show_hidden_folders") 781 paths = folders_at_path(name, True, show_hidden) 782 return report(output, keyword="paths", data=paths) 783 784 785def _api_config(name, output, kwargs): 786 """API: Dispatcher for "config" """ 787 if cfg.configlock(): 788 return report(output, _MSG_CONFIG_LOCKED) 789 return _api_config_table.get(name, (_api_config_undefined, 2))[0](output, kwargs) 790 791 792def _api_config_speedlimit(output, kwargs): 793 """API: accepts output, value(=speed)""" 794 value = kwargs.get("value") 795 if not value: 796 value = "0" 797 sabnzbd.Downloader.limit_speed(value) 798 return report(output) 799 800 801def _api_config_get_speedlimit(output, kwargs): 802 """API: accepts output""" 803 return report(output, keyword="speedlimit", data=sabnzbd.Downloader.get_limit()) 804 805 806def _api_config_set_colorscheme(output, kwargs): 807 """API: accepts output""" 808 value = kwargs.get("value") 809 if value: 810 cfg.web_color.set(value) 811 return report(output) 812 else: 813 return report(output, _MSG_NO_VALUE) 814 815 816def _api_config_set_pause(output, kwargs): 817 """API: accepts output, value(=pause interval)""" 818 value = kwargs.get("value") 819 sabnzbd.Scheduler.plan_resume(int_conv(value)) 820 return report(output) 821 822 823def _api_config_set_apikey(output, kwargs): 824 """API: accepts output""" 825 cfg.api_key.set(config.create_api_key()) 826 config.save_config() 827 return report(output, keyword="apikey", data=cfg.api_key()) 828 829 830def _api_config_set_nzbkey(output, kwargs): 831 """API: accepts output""" 832 cfg.nzb_key.set(config.create_api_key()) 833 config.save_config() 834 return report(output, keyword="nzbkey", data=cfg.nzb_key()) 835 836 837def _api_config_regenerate_certs(output, kwargs): 838 # Make sure we only over-write default locations 839 result = False 840 if ( 841 sabnzbd.cfg.https_cert() is sabnzbd.cfg.https_cert.default() 842 and sabnzbd.cfg.https_key() is sabnzbd.cfg.https_key.default() 843 ): 844 https_cert = sabnzbd.cfg.https_cert.get_path() 845 https_key = sabnzbd.cfg.https_key.get_path() 846 result = create_https_certificates(https_cert, https_key) 847 sabnzbd.RESTART_REQ = True 848 return report(output, data=result) 849 850 851def _api_config_test_server(output, kwargs): 852 """API: accepts output, server-params""" 853 result, msg = test_nntp_server_dict(kwargs) 854 response = {"result": result, "message": msg} 855 if output: 856 return report(output, data=response) 857 else: 858 return msg 859 860 861def _api_config_undefined(output, kwargs): 862 """API: accepts output""" 863 return report(output, _MSG_NOT_IMPLEMENTED) 864 865 866def _api_server_stats(name, output, kwargs): 867 """API: accepts output""" 868 sum_t, sum_m, sum_w, sum_d = sabnzbd.BPSMeter.get_sums() 869 stats = {"total": sum_t, "month": sum_m, "week": sum_w, "day": sum_d, "servers": {}} 870 871 for svr in config.get_servers(): 872 t, m, w, d, daily, articles_tried, articles_success = sabnzbd.BPSMeter.amounts(svr) 873 stats["servers"][svr] = { 874 "total": t, 875 "month": m, 876 "week": w, 877 "day": d, 878 "daily": daily, 879 "articles_tried": articles_tried, 880 "articles_success": articles_success, 881 } 882 883 return report(output, keyword="", data=stats) 884 885 886def _api_gc_stats(name, output, kwargs): 887 """Function only intended for internal testing of the memory handling""" 888 # Collect before we check 889 gc.collect() 890 # We cannot create any lists/dicts, as they would create a reference 891 return report(output, data=[str(obj) for obj in gc.get_objects() if isinstance(obj, sabnzbd.nzbstuff.TryList)]) 892 893 894############################################################################## 895_api_table = { 896 "server_stats": (_api_server_stats, 2), 897 "get_config": (_api_get_config, 3), 898 "set_config": (_api_set_config, 3), 899 "set_config_default": (_api_set_config_default, 3), 900 "del_config": (_api_del_config, 3), 901 "queue": (_api_queue, 2), 902 "options": (_api_options, 2), 903 "translate": (_api_translate, 2), 904 "addfile": (_api_addfile, 1), 905 "retry": (_api_retry, 2), 906 "cancel_pp": (_api_cancel_pp, 2), 907 "addlocalfile": (_api_addlocalfile, 1), 908 "switch": (_api_switch, 2), 909 "change_cat": (_api_change_cat, 2), 910 "change_script": (_api_change_script, 2), 911 "change_opts": (_api_change_opts, 2), 912 "fullstatus": (_api_fullstatus, 2), 913 "history": (_api_history, 2), 914 "get_files": (_api_get_files, 2), 915 "addurl": (_api_addurl, 1), 916 "addid": (_api_addurl, 1), 917 "pause": (_api_pause, 2), 918 "resume": (_api_resume, 2), 919 "shutdown": (_api_shutdown, 3), 920 "warnings": (_api_warnings, 2), 921 "config": (_api_config, 2), 922 "get_cats": (_api_get_cats, 2), 923 "get_scripts": (_api_get_scripts, 2), 924 "version": (_api_version, 1), 925 "auth": (_api_auth, 1), 926 "restart": (_api_restart, 3), 927 "restart_repair": (_api_restart_repair, 3), 928 "disconnect": (_api_disconnect, 2), 929 "osx_icon": (_api_osx_icon, 3), 930 "gc_stats": (_api_gc_stats, 3), 931 "rescan": (_api_rescan, 2), 932 "eval_sort": (_api_eval_sort, 3), 933 "watched_now": (_api_watched_now, 2), 934 "resume_pp": (_api_resume_pp, 2), 935 "pause_pp": (_api_pause_pp, 2), 936 "rss_now": (_api_rss_now, 2), 937 "browse": (_api_browse, 3), 938 "retry_all": (_api_retry_all, 2), 939 "reset_quota": (_api_reset_quota, 3), 940 "test_email": (_api_test_email, 3), 941 "test_windows": (_api_test_windows, 3), 942 "test_notif": (_api_test_notif, 3), 943 "test_osd": (_api_test_osd, 3), 944 "test_pushover": (_api_test_pushover, 3), 945 "test_pushbullet": (_api_test_pushbullet, 3), 946 "test_prowl": (_api_test_prowl, 3), 947 "test_nscript": (_api_test_nscript, 3), 948} 949 950_api_queue_table = { 951 "delete": (_api_queue_delete, 2), 952 "delete_nzf": (_api_queue_delete_nzf, 2), 953 "rename": (_api_queue_rename, 2), 954 "change_complete_action": (_api_queue_change_complete_action, 2), 955 "purge": (_api_queue_purge, 2), 956 "pause": (_api_queue_pause, 2), 957 "resume": (_api_queue_resume, 2), 958 "priority": (_api_queue_priority, 2), 959 "sort": (_api_queue_sort, 2), 960 "rating": (_api_queue_rating, 2), 961} 962 963_api_config_table = { 964 "speedlimit": (_api_config_speedlimit, 2), 965 "set_speedlimit": (_api_config_speedlimit, 2), 966 "get_speedlimit": (_api_config_get_speedlimit, 2), 967 "set_pause": (_api_config_set_pause, 2), 968 "set_colorscheme": (_api_config_set_colorscheme, 3), 969 "set_apikey": (_api_config_set_apikey, 3), 970 "set_nzbkey": (_api_config_set_nzbkey, 3), 971 "regenerate_certs": (_api_config_regenerate_certs, 3), 972 "test_server": (_api_config_test_server, 3), 973} 974 975 976def api_level(mode: str, name: str) -> int: 977 """Return access level required for this API call""" 978 if mode == "queue" and name in _api_queue_table: 979 return _api_queue_table[name][1] 980 if mode == "config" and name in _api_config_table: 981 return _api_config_table[name][1] 982 if mode in _api_table: 983 return _api_table[mode][1] 984 # It is invalid if it's none of these, but that's is handled somewhere else 985 return 4 986 987 988def report(output, error=None, keyword="value", data=None): 989 """Report message in json, xml or plain text 990 If error is set, only an status/error report is made. 991 If no error and no data, only a status report is made. 992 Else, a data report is made (optional 'keyword' for outer XML section). 993 """ 994 if output == "json": 995 content = "application/json;charset=UTF-8" 996 if error: 997 info = {"status": False, "error": error} 998 elif data is None: 999 info = {"status": True} 1000 else: 1001 if hasattr(data, "__iter__") and not keyword: 1002 info = data 1003 else: 1004 info = {keyword: data} 1005 response = utob(json.dumps(info)) 1006 1007 elif output == "xml": 1008 if not keyword: 1009 # xml always needs an outer keyword, even when json doesn't 1010 keyword = "result" 1011 content = "text/xml" 1012 xmlmaker = xml_factory() 1013 if error: 1014 status_str = xmlmaker.run("result", {"status": False, "error": error}) 1015 elif data is None: 1016 status_str = xmlmaker.run("result", {"status": True}) 1017 else: 1018 status_str = xmlmaker.run(keyword, data) 1019 response = '<?xml version="1.0" encoding="UTF-8" ?>\n%s\n' % status_str 1020 1021 else: 1022 content = "text/plain" 1023 if error: 1024 response = "error: %s\n" % error 1025 elif not data: 1026 response = "ok\n" 1027 else: 1028 response = "%s\n" % str(data) 1029 1030 cherrypy.response.headers["Content-Type"] = content 1031 cherrypy.response.headers["Pragma"] = "no-cache" 1032 return response 1033 1034 1035class xml_factory: 1036 """Recursive xml string maker. Feed it a mixed tuple/dict/item object and will output into an xml string 1037 Current limitations: 1038 In Two tiered lists hard-coded name of "item": <cat_list><item> </item></cat_list> 1039 In Three tiered lists hard-coded name of "slot": <tier1><slot><tier2> </tier2></slot></tier1> 1040 """ 1041 1042 def __init__(self): 1043 self.__text = "" 1044 1045 def _tuple(self, keyw, lst): 1046 text = [] 1047 for item in lst: 1048 text.append(self.run(keyw, item)) 1049 return "".join(text) 1050 1051 def _dict(self, keyw, lst): 1052 text = [] 1053 for key in lst.keys(): 1054 text.append(self.run(key, lst[key])) 1055 if keyw: 1056 return "<%s>%s</%s>\n" % (keyw, "".join(text), keyw) 1057 else: 1058 return "" 1059 1060 def _list(self, keyw, lst): 1061 text = [] 1062 for cat in lst: 1063 if isinstance(cat, dict): 1064 text.append(self._dict(plural_to_single(keyw, "slot"), cat)) 1065 elif isinstance(cat, list): 1066 text.append(self._list(plural_to_single(keyw, "list"), cat)) 1067 elif isinstance(cat, tuple): 1068 text.append(self._tuple(plural_to_single(keyw, "tuple"), cat)) 1069 else: 1070 if not isinstance(cat, str): 1071 cat = str(cat) 1072 name = plural_to_single(keyw, "item") 1073 text.append("<%s>%s</%s>\n" % (name, xml_name(cat), name)) 1074 if keyw: 1075 return "<%s>%s</%s>\n" % (keyw, "".join(text), keyw) 1076 else: 1077 return "" 1078 1079 def run(self, keyw, lst): 1080 if isinstance(lst, dict): 1081 text = self._dict(keyw, lst) 1082 elif isinstance(lst, list): 1083 text = self._list(keyw, lst) 1084 elif isinstance(lst, tuple): 1085 text = self._tuple(keyw, lst) 1086 elif keyw: 1087 text = "<%s>%s</%s>\n" % (keyw, xml_name(lst), keyw) 1088 else: 1089 text = "" 1090 return text 1091 1092 1093def handle_server_api(output, kwargs): 1094 """Special handler for API-call 'set_config' [servers]""" 1095 name = kwargs.get("keyword") 1096 if not name: 1097 name = kwargs.get("name") 1098 1099 if name: 1100 server = config.get_config("servers", name) 1101 if server: 1102 server.set_dict(kwargs) 1103 old_name = name 1104 else: 1105 config.ConfigServer(name, kwargs) 1106 old_name = None 1107 sabnzbd.Downloader.update_server(old_name, name) 1108 return name 1109 1110 1111def handle_rss_api(output, kwargs): 1112 """Special handler for API-call 'set_config' [rss]""" 1113 name = kwargs.get("keyword") 1114 if not name: 1115 name = kwargs.get("name") 1116 if not name: 1117 return None 1118 1119 feed = config.get_config("rss", name) 1120 if feed: 1121 feed.set_dict(kwargs) 1122 else: 1123 config.ConfigRSS(name, kwargs) 1124 1125 action = kwargs.get("filter_action") 1126 if action in ("add", "update"): 1127 # Use the general function, but catch the redirect-raise 1128 try: 1129 kwargs["feed"] = name 1130 sabnzbd.interface.ConfigRss("/").internal_upd_rss_filter(**kwargs) 1131 except cherrypy.HTTPRedirect: 1132 pass 1133 1134 elif action == "delete": 1135 # Use the general function, but catch the redirect-raise 1136 try: 1137 kwargs["feed"] = name 1138 sabnzbd.interface.ConfigRss("/").internal_del_rss_filter(**kwargs) 1139 except cherrypy.HTTPRedirect: 1140 pass 1141 1142 return name 1143 1144 1145def handle_cat_api(output, kwargs): 1146 """Special handler for API-call 'set_config' [categories]""" 1147 name = kwargs.get("keyword") 1148 if not name: 1149 name = kwargs.get("name") 1150 if not name: 1151 return None 1152 name = name.lower() 1153 1154 cat = config.get_config("categories", name) 1155 if cat: 1156 cat.set_dict(kwargs) 1157 else: 1158 config.ConfigCat(name, kwargs) 1159 return name 1160 1161 1162def build_status(skip_dashboard=False, output=None): 1163 # build up header full of basic information 1164 info = build_header(trans_functions=not output) 1165 1166 info["logfile"] = sabnzbd.LOGFILE 1167 info["weblogfile"] = sabnzbd.WEBLOGFILE 1168 info["loglevel"] = str(cfg.log_level()) 1169 info["folders"] = sabnzbd.NzbQueue.scan_jobs(all_jobs=False, action=False) 1170 info["configfn"] = config.get_filename() 1171 info["warnings"] = sabnzbd.GUIHANDLER.content() 1172 1173 # Dashboard: Speed of System 1174 info["cpumodel"] = getcpu() 1175 info["pystone"] = sabnzbd.PYSTONE_SCORE 1176 1177 # Dashboard: Speed of Download directory: 1178 info["downloaddir"] = cfg.download_dir.get_clipped_path() 1179 info["downloaddirspeed"] = sabnzbd.DOWNLOAD_DIR_SPEED 1180 1181 # Dashboard: Speed of Complete directory: 1182 info["completedir"] = cfg.complete_dir.get_clipped_path() 1183 info["completedirspeed"] = sabnzbd.COMPLETE_DIR_SPEED 1184 1185 # Dashboard: Measured download-speed 1186 info["internetbandwidth"] = sabnzbd.INTERNET_BANDWIDTH 1187 1188 # Dashboard: Connection information 1189 if not int_conv(skip_dashboard): 1190 info["localipv4"] = localipv4() 1191 info["publicipv4"] = publicipv4() 1192 info["ipv6"] = ipv6() 1193 # Dashboard: DNS-check 1194 try: 1195 addresslookup(cfg.selftest_host()) 1196 info["dnslookup"] = "OK" 1197 except: 1198 info["dnslookup"] = None 1199 1200 info["servers"] = [] 1201 # Servers-list could be modified during iteration, so we need a copy 1202 for server in sabnzbd.Downloader.servers[:]: 1203 connected = sum(nw.connected for nw in server.idle_threads[:]) 1204 serverconnections = [] 1205 for nw in server.busy_threads[:]: 1206 if nw.connected: 1207 connected += 1 1208 if nw.article: 1209 serverconnections.append( 1210 { 1211 "thrdnum": nw.thrdnum, 1212 "art_name": nw.article.article, 1213 "nzf_name": nw.article.nzf.filename, 1214 "nzo_name": nw.article.nzf.nzo.final_name, 1215 } 1216 ) 1217 1218 if server.warning and not (connected or server.errormsg): 1219 connected = server.warning 1220 1221 if server.request and not server.info: 1222 connected = T(" Resolving address").replace(" ", "") 1223 1224 server_info = { 1225 "servername": server.displayname, 1226 "serveractiveconn": connected, 1227 "servertotalconn": server.threads, 1228 "serverconnections": serverconnections, 1229 "serverssl": server.ssl, 1230 "serversslinfo": server.ssl_info, 1231 "serveractive": server.active, 1232 "servererror": server.errormsg, 1233 "serverpriority": server.priority, 1234 "serveroptional": server.optional, 1235 "serverbps": to_units(sabnzbd.BPSMeter.server_bps.get(server.id, 0)), 1236 } 1237 info["servers"].append(server_info) 1238 1239 return info 1240 1241 1242def build_queue(start=0, limit=0, trans=False, output=None, search=None, nzo_ids=None): 1243 # build up header full of basic information 1244 info, pnfo_list, bytespersec, q_size, bytes_left_previous_page = build_queue_header( 1245 search=search, start=start, limit=limit, output=output, nzo_ids=nzo_ids 1246 ) 1247 1248 datestart = datetime.datetime.now() 1249 limit = int_conv(limit) 1250 start = int_conv(start) 1251 1252 info["refresh_rate"] = str(cfg.refresh_rate()) if cfg.refresh_rate() > 0 else "" 1253 info["interface_settings"] = cfg.interface_settings() 1254 info["scripts"] = list_scripts() 1255 info["categories"] = list_cats(output is None) 1256 info["rating_enable"] = bool(cfg.rating_enable()) 1257 info["noofslots"] = q_size 1258 info["start"] = start 1259 info["limit"] = limit 1260 info["finish"] = info["start"] + info["limit"] 1261 1262 n = start 1263 running_bytes = bytes_left_previous_page 1264 slotinfo = [] 1265 for pnfo in pnfo_list: 1266 nzo_id = pnfo.nzo_id 1267 bytesleft = pnfo.bytes_left 1268 bytes_total = pnfo.bytes 1269 average_date = pnfo.avg_date 1270 is_propagating = (pnfo.avg_stamp + float(cfg.propagation_delay() * 60)) > time.time() 1271 status = pnfo.status 1272 priority = pnfo.priority 1273 mbleft = bytesleft / MEBI 1274 mb = bytes_total / MEBI 1275 1276 slot = {} 1277 slot["index"] = n 1278 slot["nzo_id"] = str(nzo_id) 1279 slot["unpackopts"] = str(opts_to_pp(pnfo.repair, pnfo.unpack, pnfo.delete)) 1280 slot["priority"] = INTERFACE_PRIORITIES.get(priority, NORMAL_PRIORITY) 1281 slot["script"] = pnfo.script if pnfo.script else "None" 1282 slot["filename"] = pnfo.filename 1283 slot["labels"] = pnfo.labels 1284 slot["password"] = pnfo.password if pnfo.password else "" 1285 slot["cat"] = pnfo.category if pnfo.category else "None" 1286 slot["mbleft"] = "%.2f" % mbleft 1287 slot["mb"] = "%.2f" % mb 1288 slot["size"] = to_units(bytes_total, "B") 1289 slot["sizeleft"] = to_units(bytesleft, "B") 1290 slot["percentage"] = "%s" % (int(((mb - mbleft) / mb) * 100)) if mb != mbleft else "0" 1291 slot["mbmissing"] = "%.2f" % (pnfo.bytes_missing / MEBI) 1292 slot["direct_unpack"] = pnfo.direct_unpack 1293 if not output: 1294 slot["mb_fmt"] = locale.format_string("%d", int(mb), True) 1295 slot["mbdone_fmt"] = locale.format_string("%d", int(mb - mbleft), True) 1296 1297 if not sabnzbd.Downloader.paused and status not in (Status.PAUSED, Status.FETCHING, Status.GRABBING): 1298 if is_propagating: 1299 slot["status"] = Status.PROP 1300 elif status == Status.CHECKING: 1301 slot["status"] = Status.CHECKING 1302 else: 1303 slot["status"] = Status.DOWNLOADING 1304 else: 1305 # Ensure compatibility of API status 1306 if status == Status.DELETED or priority == FORCE_PRIORITY: 1307 status = Status.DOWNLOADING 1308 slot["status"] = "%s" % status 1309 1310 if ( 1311 sabnzbd.Downloader.paused 1312 or sabnzbd.Downloader.paused_for_postproc 1313 or is_propagating 1314 or status not in (Status.DOWNLOADING, Status.FETCHING, Status.QUEUED) 1315 ) and priority != FORCE_PRIORITY: 1316 slot["timeleft"] = "0:00:00" 1317 slot["eta"] = "unknown" 1318 else: 1319 running_bytes += bytesleft 1320 slot["timeleft"] = calc_timeleft(running_bytes, bytespersec) 1321 try: 1322 datestart = datestart + datetime.timedelta(seconds=bytesleft / bytespersec) 1323 # new eta format: 16:00 Fri 07 Feb 1324 slot["eta"] = datestart.strftime(time_format("%H:%M %a %d %b")) 1325 except: 1326 datestart = datetime.datetime.now() 1327 slot["eta"] = "unknown" 1328 1329 # Do not show age when it's not known 1330 if average_date.year < 2000: 1331 slot["avg_age"] = "-" 1332 else: 1333 slot["avg_age"] = calc_age(average_date, bool(trans)) 1334 1335 rating = sabnzbd.Rating.get_rating_by_nzo(nzo_id) 1336 slot["has_rating"] = rating is not None 1337 if rating: 1338 slot["rating_avg_video"] = rating.avg_video 1339 slot["rating_avg_audio"] = rating.avg_audio 1340 1341 slotinfo.append(slot) 1342 n += 1 1343 1344 if slotinfo: 1345 info["slots"] = slotinfo 1346 else: 1347 info["slots"] = [] 1348 1349 return info, pnfo_list, bytespersec 1350 1351 1352def fast_queue() -> Tuple[bool, int, float, str]: 1353 """Return paused, bytes_left, bpsnow, time_left""" 1354 bytes_left = sabnzbd.sabnzbd.NzbQueue.remaining() 1355 paused = sabnzbd.Downloader.paused 1356 bpsnow = sabnzbd.BPSMeter.bps 1357 time_left = calc_timeleft(bytes_left, bpsnow) 1358 return paused, bytes_left, bpsnow, time_left 1359 1360 1361def build_file_list(nzo_id: str): 1362 """Build file lists for specified job""" 1363 jobs = [] 1364 nzo = sabnzbd.sabnzbd.NzbQueue.get_nzo(nzo_id) 1365 if nzo: 1366 pnfo = nzo.gather_info(full=True) 1367 1368 finished_files = pnfo.finished_files 1369 active_files = pnfo.active_files 1370 queued_files = pnfo.queued_files 1371 1372 for nzf in finished_files: 1373 jobs.append( 1374 { 1375 "filename": nzf.filename, 1376 "mbleft": "%.2f" % (nzf.bytes_left / MEBI), 1377 "mb": "%.2f" % (nzf.bytes / MEBI), 1378 "bytes": "%.2f" % nzf.bytes, 1379 "age": calc_age(nzf.date), 1380 "nzf_id": nzf.nzf_id, 1381 "status": "finished", 1382 } 1383 ) 1384 1385 for nzf in active_files: 1386 jobs.append( 1387 { 1388 "filename": nzf.filename, 1389 "mbleft": "%.2f" % (nzf.bytes_left / MEBI), 1390 "mb": "%.2f" % (nzf.bytes / MEBI), 1391 "bytes": "%.2f" % nzf.bytes, 1392 "age": calc_age(nzf.date), 1393 "nzf_id": nzf.nzf_id, 1394 "status": "active", 1395 } 1396 ) 1397 1398 for nzf in queued_files: 1399 jobs.append( 1400 { 1401 "filename": nzf.filename, 1402 "set": nzf.setname, 1403 "mbleft": "%.2f" % (nzf.bytes_left / MEBI), 1404 "mb": "%.2f" % (nzf.bytes / MEBI), 1405 "bytes": "%.2f" % nzf.bytes, 1406 "age": calc_age(nzf.date), 1407 "nzf_id": nzf.nzf_id, 1408 "status": "queued", 1409 } 1410 ) 1411 1412 return jobs 1413 1414 1415def options_list(output): 1416 return report( 1417 output, 1418 keyword="options", 1419 data={ 1420 "sabyenc": sabnzbd.decoder.SABYENC_ENABLED, 1421 "par2": sabnzbd.newsunpack.PAR2_COMMAND, 1422 "multipar": sabnzbd.newsunpack.MULTIPAR_COMMAND, 1423 "rar": sabnzbd.newsunpack.RAR_COMMAND, 1424 "zip": sabnzbd.newsunpack.ZIP_COMMAND, 1425 "7zip": sabnzbd.newsunpack.SEVEN_COMMAND, 1426 "nice": sabnzbd.newsunpack.NICE_COMMAND, 1427 "ionice": sabnzbd.newsunpack.IONICE_COMMAND, 1428 }, 1429 ) 1430 1431 1432def retry_job(job, new_nzb=None, password=None): 1433 """Re enter failed job in the download queue""" 1434 if job: 1435 history_db = sabnzbd.get_db_connection() 1436 futuretype, url, pp, script, cat = history_db.get_other(job) 1437 if futuretype: 1438 nzo_id = sabnzbd.add_url(url, pp, script, cat) 1439 else: 1440 path = history_db.get_path(job) 1441 nzo_id = sabnzbd.NzbQueue.repair_job(path, new_nzb, password) 1442 if nzo_id: 1443 # Only remove from history if we repaired something 1444 history_db.remove_history(job) 1445 return nzo_id 1446 return None 1447 1448 1449def retry_all_jobs(): 1450 """Re enter all failed jobs in the download queue""" 1451 # Fetch all retryable folders from History 1452 items = sabnzbd.api.build_history()[0] 1453 nzo_ids = [] 1454 for item in items: 1455 if item["retry"]: 1456 nzo_ids.append(retry_job(item["nzo_id"])) 1457 return nzo_ids 1458 1459 1460def del_job_files(job_paths): 1461 """Remove files of each path in the list""" 1462 for path in job_paths: 1463 if path and clip_path(path).lower().startswith(cfg.download_dir.get_clipped_path().lower()): 1464 remove_all(path, recursive=True) 1465 1466 1467def del_hist_job(job, del_files): 1468 """Remove history element""" 1469 if job: 1470 path = sabnzbd.PostProcessor.get_path(job) 1471 if path: 1472 sabnzbd.PostProcessor.delete(job, del_files=del_files) 1473 else: 1474 history_db = sabnzbd.get_db_connection() 1475 remove_all(history_db.get_path(job), recursive=True) 1476 history_db.remove_history(job) 1477 1478 1479def Tspec(txt): 1480 """Translate special terms""" 1481 if txt == "None": 1482 return T("None") 1483 elif txt in ("Default", "*"): 1484 return T("Default") 1485 else: 1486 return txt 1487 1488 1489_SKIN_CACHE = {} # Stores pre-translated acronyms 1490 1491 1492def Ttemplate(txt): 1493 """Translation function for Skin texts 1494 This special is to be used in interface.py for template processing 1495 to be passed for the $T function: so { ..., 'T' : Ttemplate, ...} 1496 """ 1497 global _SKIN_CACHE 1498 if txt in _SKIN_CACHE: 1499 return _SKIN_CACHE[txt] 1500 else: 1501 # We need to remove the " and ' to be JS/JSON-string-safe 1502 # Saving it in dictionary is 20x faster on next look-up 1503 tra = T(SKIN_TEXT.get(txt, txt)).replace('"', """).replace("'", "'") 1504 _SKIN_CACHE[txt] = tra 1505 return tra 1506 1507 1508def clear_trans_cache(): 1509 """Clean cache for skin translations""" 1510 global _SKIN_CACHE 1511 _SKIN_CACHE = {} 1512 sabnzbd.WEBUI_READY = True 1513 1514 1515def build_header(webdir="", output=None, trans_functions=True): 1516 """Build the basic header""" 1517 try: 1518 uptime = calc_age(sabnzbd.START) 1519 except: 1520 uptime = "-" 1521 1522 speed_limit = sabnzbd.Downloader.get_limit() 1523 if speed_limit <= 0: 1524 speed_limit = 100 1525 speed_limit_abs = sabnzbd.Downloader.get_limit_abs() 1526 if speed_limit_abs <= 0: 1527 speed_limit_abs = "" 1528 1529 diskspace_info = diskspace() 1530 1531 header = {} 1532 1533 # We don't output everything for API 1534 if not output: 1535 # These are functions, and cause problems for JSON 1536 if trans_functions: 1537 header["T"] = Ttemplate 1538 header["Tspec"] = Tspec 1539 1540 header["uptime"] = uptime 1541 header["color_scheme"] = sabnzbd.WEB_COLOR or "" 1542 header["helpuri"] = "https://sabnzbd.org/wiki/" 1543 1544 header["restart_req"] = sabnzbd.RESTART_REQ 1545 header["pid"] = os.getpid() 1546 header["active_lang"] = cfg.language() 1547 header["rtl"] = is_rtl(header["active_lang"]) 1548 1549 header["my_lcldata"] = clip_path(sabnzbd.DIR_LCLDATA) 1550 header["my_home"] = clip_path(sabnzbd.DIR_HOME) 1551 header["webdir"] = webdir or sabnzbd.WEB_DIR 1552 header["url_base"] = cfg.url_base() 1553 1554 header["nt"] = sabnzbd.WIN32 1555 header["darwin"] = sabnzbd.DARWIN 1556 1557 header["power_options"] = sabnzbd.WIN32 or sabnzbd.DARWIN or sabnzbd.LINUX_POWER 1558 header["pp_pause_event"] = sabnzbd.Scheduler.pp_pause_event 1559 1560 header["apikey"] = cfg.api_key() 1561 header["new_release"], header["new_rel_url"] = sabnzbd.NEW_VERSION 1562 1563 header["version"] = sabnzbd.__version__ 1564 header["paused"] = bool(sabnzbd.Downloader.paused or sabnzbd.Downloader.paused_for_postproc) 1565 header["pause_int"] = sabnzbd.Scheduler.pause_int() 1566 header["paused_all"] = sabnzbd.PAUSED_ALL 1567 1568 header["diskspace1"] = "%.2f" % diskspace_info["download_dir"][1] 1569 header["diskspace2"] = "%.2f" % diskspace_info["complete_dir"][1] 1570 header["diskspace1_norm"] = to_units(diskspace_info["download_dir"][1] * GIGI) 1571 header["diskspace2_norm"] = to_units(diskspace_info["complete_dir"][1] * GIGI) 1572 header["diskspacetotal1"] = "%.2f" % diskspace_info["download_dir"][0] 1573 header["diskspacetotal2"] = "%.2f" % diskspace_info["complete_dir"][0] 1574 header["loadavg"] = loadavg() 1575 header["speedlimit"] = "{1:0.{0}f}".format(int(speed_limit % 1 > 0), speed_limit) 1576 header["speedlimit_abs"] = "%s" % speed_limit_abs 1577 1578 header["have_warnings"] = str(sabnzbd.GUIHANDLER.count()) 1579 header["finishaction"] = sabnzbd.QUEUECOMPLETE 1580 1581 header["quota"] = to_units(sabnzbd.BPSMeter.quota) 1582 header["have_quota"] = bool(sabnzbd.BPSMeter.quota > 0.0) 1583 header["left_quota"] = to_units(sabnzbd.BPSMeter.left) 1584 1585 anfo = sabnzbd.ArticleCache.cache_info() 1586 header["cache_art"] = str(anfo.article_sum) 1587 header["cache_size"] = to_units(anfo.cache_size, "B") 1588 header["cache_max"] = str(anfo.cache_limit) 1589 1590 return header 1591 1592 1593def build_queue_header(search=None, nzo_ids=None, start=0, limit=0, output=None): 1594 """Build full queue header""" 1595 1596 header = build_header(output=output) 1597 1598 bytespersec = sabnzbd.BPSMeter.bps 1599 qnfo = sabnzbd.NzbQueue.queue_info(search=search, nzo_ids=nzo_ids, start=start, limit=limit) 1600 1601 bytesleft = qnfo.bytes_left 1602 bytes_total = qnfo.bytes 1603 1604 header["kbpersec"] = "%.2f" % (bytespersec / KIBI) 1605 header["speed"] = to_units(bytespersec) 1606 header["mbleft"] = "%.2f" % (bytesleft / MEBI) 1607 header["mb"] = "%.2f" % (bytes_total / MEBI) 1608 header["sizeleft"] = to_units(bytesleft, "B") 1609 header["size"] = to_units(bytes_total, "B") 1610 header["noofslots_total"] = qnfo.q_fullsize 1611 1612 if sabnzbd.Downloader.paused or sabnzbd.Downloader.paused_for_postproc: 1613 status = Status.PAUSED 1614 elif bytespersec > 0: 1615 status = Status.DOWNLOADING 1616 else: 1617 status = "Idle" 1618 header["status"] = status 1619 header["timeleft"] = calc_timeleft(bytesleft, bytespersec) 1620 1621 try: 1622 datestart = datetime.datetime.now() + datetime.timedelta(seconds=bytesleft / bytespersec) 1623 # new eta format: 16:00 Fri 07 Feb 1624 header["eta"] = datestart.strftime(time_format("%H:%M %a %d %b")) 1625 except: 1626 header["eta"] = T("unknown") 1627 1628 return header, qnfo.list, bytespersec, qnfo.q_fullsize, qnfo.bytes_left_previous_page 1629 1630 1631def build_history( 1632 start: int = 0, 1633 limit: int = 0, 1634 search: Optional[str] = None, 1635 failed_only: int = 0, 1636 categories: Optional[List[str]] = None, 1637 nzo_ids: Optional[List[str]] = None, 1638): 1639 """Combine the jobs still in post-processing and the database history""" 1640 if not limit: 1641 limit = 1000000 1642 1643 # Grab any items that are active or queued in postproc 1644 postproc_queue = sabnzbd.PostProcessor.get_queue() 1645 1646 # Filter out any items that don't match the search term or category 1647 if postproc_queue: 1648 # It would be more efficient to iterate only once, but we accept the penalty for code clarity 1649 if isinstance(search, list): 1650 postproc_queue = [nzo for nzo in postproc_queue if nzo.cat in categories] 1651 1652 if isinstance(search, str): 1653 # Replace * with .* and ' ' with . 1654 search_text = search.strip().replace("*", ".*").replace(" ", ".*") + ".*?" 1655 try: 1656 re_search = re.compile(search_text, re.I) 1657 postproc_queue = [nzo for nzo in postproc_queue if re_search.search(nzo.final_name)] 1658 except: 1659 logging.error(T("Failed to compile regex for search term: %s"), search_text) 1660 1661 if nzo_ids: 1662 postproc_queue = [nzo for nzo in postproc_queue if nzo.nzo_id in nzo_ids] 1663 1664 # Multi-page support for postproc items 1665 postproc_queue_size = len(postproc_queue) 1666 if start > postproc_queue_size: 1667 # On a page where we shouldn't show postproc items 1668 postproc_queue = [] 1669 database_history_limit = limit 1670 else: 1671 try: 1672 if limit: 1673 postproc_queue = postproc_queue[start : start + limit] 1674 else: 1675 postproc_queue = postproc_queue[start:] 1676 except: 1677 pass 1678 # Remove the amount of postproc items from the db request for history items 1679 database_history_limit = max(limit - len(postproc_queue), 0) 1680 database_history_start = max(start - postproc_queue_size, 0) 1681 1682 # Acquire the db instance 1683 try: 1684 history_db = sabnzbd.get_db_connection() 1685 close_db = False 1686 except: 1687 # Required for repairs at startup because Cherrypy isn't active yet 1688 history_db = HistoryDB() 1689 close_db = True 1690 1691 # Fetch history items 1692 if not database_history_limit: 1693 items, fetched_items, total_items = history_db.fetch_history( 1694 database_history_start, 1, search, failed_only, categories, nzo_ids 1695 ) 1696 items = [] 1697 else: 1698 items, fetched_items, total_items = history_db.fetch_history( 1699 database_history_start, database_history_limit, search, failed_only, categories, nzo_ids 1700 ) 1701 1702 # Reverse the queue to add items to the top (faster than insert) 1703 items.reverse() 1704 1705 # Add the postproc items to the top of the history 1706 items = get_active_history(postproc_queue, items) 1707 1708 # Un-reverse the queue 1709 items.reverse() 1710 1711 # Global check if rating is enabled 1712 rating_enabled = cfg.rating_enable() 1713 1714 for item in items: 1715 item["size"] = to_units(item["bytes"], "B") 1716 1717 if "loaded" not in item: 1718 item["loaded"] = False 1719 1720 path = item.get("path", "") 1721 item["retry"] = int_conv(item.get("status") == Status.FAILED and path and os.path.exists(path)) 1722 # Retry of failed URL-fetch 1723 if item["report"] == "future": 1724 item["retry"] = True 1725 1726 if rating_enabled: 1727 rating = sabnzbd.Rating.get_rating_by_nzo(item["nzo_id"]) 1728 item["has_rating"] = rating is not None 1729 if rating: 1730 item["rating_avg_video"] = rating.avg_video 1731 item["rating_avg_audio"] = rating.avg_audio 1732 item["rating_avg_vote_up"] = rating.avg_vote_up 1733 item["rating_avg_vote_down"] = rating.avg_vote_down 1734 item["rating_user_video"] = rating.user_video 1735 item["rating_user_audio"] = rating.user_audio 1736 item["rating_user_vote"] = rating.user_vote 1737 1738 total_items += postproc_queue_size 1739 fetched_items = len(items) 1740 1741 if close_db: 1742 history_db.close() 1743 1744 return items, fetched_items, total_items 1745 1746 1747def get_active_history(queue, items): 1748 """Get the currently in progress and active history queue.""" 1749 for nzo in queue: 1750 item = {} 1751 ( 1752 item["completed"], 1753 item["name"], 1754 item["nzb_name"], 1755 item["category"], 1756 item["pp"], 1757 item["script"], 1758 item["report"], 1759 item["url"], 1760 item["status"], 1761 item["nzo_id"], 1762 item["storage"], 1763 item["path"], 1764 item["script_log"], 1765 item["script_line"], 1766 item["download_time"], 1767 item["postproc_time"], 1768 item["stage_log"], 1769 item["downloaded"], 1770 item["fail_message"], 1771 item["url_info"], 1772 item["bytes"], 1773 _, 1774 _, 1775 item["password"], 1776 ) = build_history_info(nzo) 1777 item["action_line"] = nzo.action_line 1778 item = unpack_history_info(item) 1779 1780 item["loaded"] = nzo.pp_active 1781 if item["bytes"]: 1782 item["size"] = to_units(item["bytes"], "B") 1783 else: 1784 item["size"] = "" 1785 items.append(item) 1786 1787 return items 1788 1789 1790def calc_timeleft(bytesleft, bps): 1791 """Calculate the time left in the format HH:MM:SS""" 1792 try: 1793 if bytesleft <= 0: 1794 return "0:00:00" 1795 totalseconds = int(bytesleft / bps) 1796 minutes, seconds = divmod(totalseconds, 60) 1797 hours, minutes = divmod(minutes, 60) 1798 days, hours = divmod(hours, 24) 1799 if minutes < 10: 1800 minutes = "0%s" % minutes 1801 if seconds < 10: 1802 seconds = "0%s" % seconds 1803 if days > 0: 1804 if hours < 10: 1805 hours = "0%s" % hours 1806 return "%s:%s:%s:%s" % (days, hours, minutes, seconds) 1807 else: 1808 return "%s:%s:%s" % (hours, minutes, seconds) 1809 except: 1810 return "0:00:00" 1811 1812 1813def list_cats(default=True): 1814 """Return list of (ordered) categories, 1815 when default==False use '*' for Default category 1816 """ 1817 lst = [cat["name"] for cat in config.get_ordered_categories()] 1818 if default: 1819 lst.remove("*") 1820 lst.insert(0, "Default") 1821 return lst 1822 1823 1824_PLURAL_TO_SINGLE = { 1825 "categories": "category", 1826 "servers": "server", 1827 "rss": "feed", 1828 "scripts": "script", 1829 "warnings": "warning", 1830 "files": "file", 1831 "jobs": "job", 1832} 1833 1834 1835def plural_to_single(kw, def_kw=""): 1836 try: 1837 return _PLURAL_TO_SINGLE[kw] 1838 except KeyError: 1839 return def_kw 1840 1841 1842def del_from_section(kwargs): 1843 """Remove keyword in section""" 1844 section = kwargs.get("section", "") 1845 if section in ("servers", "rss", "categories"): 1846 keyword = kwargs.get("keyword") 1847 if keyword: 1848 item = config.get_config(section, keyword) 1849 if item: 1850 item.delete() 1851 del item 1852 config.save_config() 1853 if section == "servers": 1854 sabnzbd.Downloader.update_server(keyword, None) 1855 return True 1856 else: 1857 return False 1858 1859 1860def history_remove_failed(): 1861 """Remove all failed jobs from history, including files""" 1862 logging.info("Scheduled removal of all failed jobs") 1863 with HistoryDB() as history_db: 1864 del_job_files(history_db.get_failed_paths()) 1865 history_db.remove_failed() 1866 1867 1868def history_remove_completed(): 1869 """Remove all completed jobs from history""" 1870 logging.info("Scheduled removal of all completed jobs") 1871 with HistoryDB() as history_db: 1872 history_db.remove_completed() 1873