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("&nbsp;Resolving address").replace("&nbsp;", "")
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('"', "&quot;").replace("'", "&apos;")
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