From 4002046e31f8474ac2e0382e7d565c662fcafe00 Mon Sep 17 00:00:00 2001 From: bobokun Date: Sat, 13 Aug 2022 14:33:37 -0400 Subject: [PATCH 01/11] 3.2.5 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 9b7a431d..448ada3b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.2.4 \ No newline at end of file +3.2.5 \ No newline at end of file From 41e9893bf553cda62b8791dbf60ebc52dc64831b Mon Sep 17 00:00:00 2001 From: bobokun Date: Sat, 13 Aug 2022 14:35:00 -0400 Subject: [PATCH 02/11] update requirements.txt --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d79a414e..adf44697 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ ruamel.yaml==0.17.21 -qbittorrent-api>=2022.8.34 +qbittorrent-api>=2022.8.35 schedule==1.1.0 retrying==1.3.3 alive_progress==2.4.1 From dddafaf0ed6b736d5fc9418a1894809551ea0ba0 Mon Sep 17 00:00:00 2001 From: bobokun Date: Sat, 13 Aug 2022 16:49:02 -0400 Subject: [PATCH 03/11] YAML R/W fixes + Adds new FR #146 --- config/config.yml.sample | 46 +++++++++++++--------- modules/config.py | 84 +++++++++++++++++++--------------------- modules/qbittorrent.py | 72 +++++++++++++++++++++------------- modules/util.py | 60 +++++++++++++++++++++------- 4 files changed, 159 insertions(+), 103 deletions(-) diff --git a/config/config.yml.sample b/config/config.yml.sample index 3a04bbb0..ebebcaa2 100644 --- a/config/config.yml.sample +++ b/config/config.yml.sample @@ -3,8 +3,8 @@ # Please refer to the link below for more details on how to set up the configuration file # https://github.com/StuffAnThings/qbit_manage/wiki/Config-Setup -# qBittorrent parameters qbt: + # qBittorrent parameters host: "localhost:8080" user: "username" pass: "password" @@ -31,14 +31,24 @@ directory: recycle_bin: "/mnt/user/data/torrents/.RecycleBin" torrents_dir: "/qbittorrent/data/BT_backup" -# Category & Path Parameters cat: + # Category & Path Parameters # : # Path of your save directory. movies: "/data/torrents/Movies" tv: "/data/torrents/TV" -# Tag Parameters +cat_change: + # This moves all the torrents from one category to another category. This executes on --cat-update + # WARNING: if the paths are different and Default Torrent Management Mode is set to automatic the files could be moved !!! + # : + Radarr-HD.cross-seed: movies-hd + Radarr-UHD.cross-seed: movies-uhd + movies-hd.cross-seed: movies-hd + movies-uhd.cross-seed: movies-uhd + + tracker: + # Tag Parameters # : # This is the keyword in the tracker url # Set tag name. Can be a list of tags or a single tag # tag: @@ -103,8 +113,8 @@ tracker: tv-vault: tag: TV-Vault -# Tag Movies/Series that are not hard linked nohardlinks: + # Tag Movies/Series that are not hard linked # Mandatory to fill out directory parameter above to use this function (root_dir/remote_dir) # This variable should be set to your category name of your completed movies/completed series in qbit. Acceptable variable can be any category you would like to tag if there are no hardlinks found movies-completed: @@ -141,9 +151,9 @@ nohardlinks: # min seeding time var: Will ensure that torrent is not deleted by cleanup variable if torrent does not meet minimum seeding time (min). min_seeding_time: 43200 -# Recycle Bin method of deletion will move files into the recycle bin (Located in /root_dir/.RecycleBin) instead of directly deleting them in qbit -# By default the Recycle Bin will be emptied on every run of the qbit_manage script if empty_after_x_days is defined. recyclebin: + # Recycle Bin method of deletion will move files into the recycle bin (Located in /root_dir/.RecycleBin) instead of directly deleting them in qbit + # By default the Recycle Bin will be emptied on every run of the qbit_manage script if empty_after_x_days is defined. enabled: true # empty_after_x_days var: # Will automatically remove all files and folders in recycle bin after x days. (Checks every script run) @@ -159,8 +169,8 @@ recyclebin: # and add the base folder name of the recycle bin that was defined in the `recycle_bin` sub-attribute under directory. split_by_category: false -# Orphaned files are those in the root_dir download directory that are not referenced by any active torrents. orphaned: + # Orphaned files are those in the root_dir download directory that are not referenced by any active torrents. # File patterns that will not be considered orphaned files. Handy for generated files that aren't part of the torrent but belong with the torrent's files exclude_patterns: - "**/.DS_Store" @@ -169,29 +179,29 @@ orphaned: - "/data/torrents/temp/**" - "**/*.!qB" -# Apprise integration with webhooks -# Leave Empty/Blank to disable apprise: + # Apprise integration with webhooks + # Leave Empty/Blank to disable # Mandatory to fill out the url of your apprise API endpoint api_url: http://apprise-api:8000 # Mandatory to fill out the notification url/urls based on the notification services provided by apprise. https://github.com/caronc/apprise/wiki notify_url: -# Notifiarr integration with webhooks -# Leave Empty/Blank to disable notifiarr: + # Notifiarr integration with webhooks + # Leave Empty/Blank to disable # Mandatory to fill out API Key apikey: #################################### # Set to a unique value (could be your username on notifiarr for example) instance: -# Webhook notifications: -# Possible values: -# Set value to notifiarr if using notifiarr integration -# Set value to apprise if using apprise integration -# Set value to a valid webhook URL -# Leave Empty/Blank to disable webhooks: + # Webhook notifications: + # Possible values: + # Set value to notifiarr if using notifiarr integration + # Set value to apprise if using apprise integration + # Set value to a valid webhook URL + # Leave Empty/Blank to disable error: https://mywebhookurl.com/qbt_manage run_start: notifiarr run_end: apprise @@ -206,6 +216,6 @@ webhooks: tag_nohardlinks: notifiarr empty_recyclebin: notifiarr -# BHD Integration used for checking unregistered torrents bhd: + # BHD Integration used for checking unregistered torrents apikey: \ No newline at end of file diff --git a/modules/config.py b/modules/config.py index 1fa6acd9..6612dc87 100644 --- a/modules/config.py +++ b/modules/config.py @@ -1,12 +1,11 @@ import logging, os, requests, stat, time, re from modules import util -from modules.util import Failed, check +from modules.util import Failed, check, YAML from modules.qbittorrent import Qbt from modules.webhooks import Webhooks from modules.notifiarr import Notifiarr from modules.bhd import BeyondHD from modules.apprise import Apprise -from ruamel import yaml from retrying import retry logger = logging.getLogger("qBit Manage") @@ -30,50 +29,43 @@ def __init__(self, default_dir, args): self.trace_mode = args["trace"] if "trace" in args else False self.start_time = args["time_obj"] - yaml.YAML().allow_duplicate_keys = True - try: - new_config, _, _ = yaml.util.load_yaml_guess_indent(open(self.config_path, encoding="utf-8")) - if "qbt" in new_config: new_config["qbt"] = new_config.pop("qbt") - new_config["settings"] = new_config.pop("settings") if "settings" in new_config else {} - if "directory" in new_config: new_config["directory"] = new_config.pop("directory") - new_config["cat"] = new_config.pop("cat") if "cat" in new_config else {} - if "tracker" in new_config: new_config["tracker"] = new_config.pop("tracker") - elif "tags" in new_config: new_config["tracker"] = new_config.pop("tags") - else: new_config["tracker"] = {} - if "nohardlinks" in new_config: new_config["nohardlinks"] = new_config.pop("nohardlinks") - if "recyclebin" in new_config: new_config["recyclebin"] = new_config.pop("recyclebin") - if "orphaned" in new_config: new_config["orphaned"] = new_config.pop("orphaned") - if "apprise" in new_config: new_config["apprise"] = new_config.pop("apprise") - if "notifiarr" in new_config: new_config["notifiarr"] = new_config.pop("notifiarr") - if "webhooks" in new_config: - temp = new_config.pop("webhooks") - if 'function' not in temp or ('function' in temp and temp['function'] is None): temp["function"] = {} + loaded_yaml = YAML(self.config_path) + self.data = loaded_yaml.data + if "qbt" in self.data: self.data["qbt"] = self.data.pop("qbt") + self.data["settings"] = self.data.pop("settings") if "settings" in self.data else {} + if "directory" in self.data: self.data["directory"] = self.data.pop("directory") + self.data["cat"] = self.data.pop("cat") if "cat" in self.data else {} + if "cat_change" in self.data: self.data["cat_change"] = self.data.pop("cat_change") + if "tracker" in self.data: self.data["tracker"] = self.data.pop("tracker") + elif "tags" in self.data: self.data["tracker"] = self.data.pop("tags") + else: self.data["tracker"] = {} + if "nohardlinks" in self.data: self.data["nohardlinks"] = self.data.pop("nohardlinks") + if "recyclebin" in self.data: self.data["recyclebin"] = self.data.pop("recyclebin") + if "orphaned" in self.data: self.data["orphaned"] = self.data.pop("orphaned") + if "apprise" in self.data: self.data["apprise"] = self.data.pop("apprise") + if "notifiarr" in self.data: self.data["notifiarr"] = self.data.pop("notifiarr") + if "webhooks" in self.data: + temp = self.data.pop("webhooks") + if 'function' not in temp or ('function' in temp and temp['function'] is None): temp["function"] = {} - def hooks(attr): - if attr in temp: - items = temp.pop(attr) - if items: - temp["function"][attr] = items - if attr not in temp["function"]: - temp["function"][attr] = {} - temp["function"][attr] = None - hooks("cross_seed") - hooks("recheck") - hooks("cat_update") - hooks("tag_update") - hooks("rem_unregistered") - hooks("rem_orphaned") - hooks("tag_nohardlinks") - hooks("empty_recyclebin") - new_config["webhooks"] = temp - if "bhd" in new_config: new_config["bhd"] = new_config.pop("bhd") - yaml.round_trip_dump(new_config, open(self.config_path, "w", encoding="utf-8"), indent=None, block_seq_indent=2) - self.data = new_config - except yaml.scanner.ScannerError as e: - raise Failed(f"YAML Error: {util.tab_new_lines(e)}") - except Exception as e: - util.print_stacktrace() - raise Failed(f"YAML Error: {e}") + def hooks(attr): + if attr in temp: + items = temp.pop(attr) + if items: + temp["function"][attr] = items + if attr not in temp["function"]: + temp["function"][attr] = {} + temp["function"][attr] = None + hooks("cross_seed") + hooks("recheck") + hooks("cat_update") + hooks("tag_update") + hooks("rem_unregistered") + hooks("rem_orphaned") + hooks("tag_nohardlinks") + hooks("empty_recyclebin") + self.data["webhooks"] = temp + if "bhd" in self.data: self.data["bhd"] = self.data.pop("bhd") self.session = requests.Session() @@ -105,6 +97,8 @@ def hooks(attr): for func in default_function: self.util.check_for_attribute(self.data, func, parent="webhooks", subparent="function", default_is_none=True) + self.cat_change = self.data["cat_change"] if "cat_change" in self.data else {} + self.AppriseFactory = None if "apprise" in self.data: if self.data["apprise"] is not None: diff --git a/modules/qbittorrent.py b/modules/qbittorrent.py index afbb2acb..2648b778 100644 --- a/modules/qbittorrent.py +++ b/modules/qbittorrent.py @@ -161,37 +161,57 @@ def category(self): dry_run = self.config.args['dry_run'] loglevel = 'DRYRUN' if dry_run else 'INFO' num_cat = 0 + + def update_cat(new_cat, cat_change): + nonlocal dry_run, torrent, num_cat + tracker = self.config.get_tags([x.url for x in torrent.trackers if x.url.startswith('http')]) + old_cat = torrent.category + if not dry_run: + try: + torrent.set_category(category=new_cat) + if torrent.auto_tmm is False and self.config.settings['force_auto_tmm']: + torrent.set_auto_management(True) + except Conflict409Error: + e = print_line(f'Existing category "{new_cat}" not found for save path {torrent.save_path}, category will be created.', loglevel) + self.config.notify(e, 'Update Category', False) + self.client.torrent_categories.create_category(name=new_cat, save_path=torrent.save_path) + torrent.set_category(category=new_cat) + body = [] + body += print_line(util.insert_space(f'Torrent Name: {torrent.name}', 3), loglevel) + if cat_change: + body += print_line(util.insert_space(f'Old Category: {old_cat}', 3), loglevel) + title = "Moving Categories" + else: + title = "Updating Categories" + body += print_line(util.insert_space(f'New Category: {new_cat}', 3), loglevel) + body += print_line(util.insert_space(f'Tracker: {tracker["url"]}', 8), loglevel) + attr = { + "function": "cat_update", + "title": title, + "body": "\n".join(body), + "torrent_name": torrent.name, + "torrent_category": new_cat, + "torrent_tracker": tracker["url"], + "notifiarr_indexer": tracker["notifiarr"] + } + self.config.send_notifications(attr) + num_cat += 1 + if self.config.args['cat_update']: separator("Updating Categories", space=False, border=False) torrent_list = self.get_torrents({'category': '', 'filter': 'completed'}) for torrent in torrent_list: new_cat = self.config.get_category(torrent.save_path) - tracker = self.config.get_tags([x.url for x in torrent.trackers if x.url.startswith('http')]) - if not dry_run: - try: - torrent.set_category(category=new_cat) - if torrent.auto_tmm is False and self.config.settings['force_auto_tmm']: - torrent.set_auto_management(True) - except Conflict409Error: - e = print_line(f'Existing category "{new_cat}" not found for save path {torrent.save_path}, category will be created.', loglevel) - self.config.notify(e, 'Update Category', False) - self.client.torrent_categories.create_category(name=new_cat, save_path=torrent.save_path) - torrent.set_category(category=new_cat) - body = [] - body += print_line(util.insert_space(f'Torrent Name: {torrent.name}', 3), loglevel) - body += print_line(util.insert_space(f'New Category: {new_cat}', 3), loglevel) - body += print_line(util.insert_space(f'Tracker: {tracker["url"]}', 8), loglevel) - attr = { - "function": "cat_update", - "title": "Updating Categories", - "body": "\n".join(body), - "torrent_name": torrent.name, - "torrent_category": new_cat, - "torrent_tracker": tracker["url"], - "notifiarr_indexer": tracker["notifiarr"] - } - self.config.send_notifications(attr) - num_cat += 1 + update_cat(new_cat, False) + + # Change categories + if self.config.cat_change: + for old_cat in self.config.cat_change: + torrent_list = self.get_torrents({'category': old_cat, 'filter': 'completed'}) + for torrent in torrent_list: + new_cat = self.config.cat_change[old_cat] + update_cat(new_cat, True) + if num_cat >= 1: print_line(f"{'Did not update' if dry_run else 'Updated'} {num_cat} new categories.", loglevel) else: diff --git a/modules/util.py b/modules/util.py index fdaf1029..2c60d191 100644 --- a/modules/util.py +++ b/modules/util.py @@ -1,6 +1,5 @@ -import logging, os, shutil, traceback, time, signal, json +import logging, os, shutil, traceback, time, signal, json, ruamel.yaml from logging.handlers import RotatingFileHandler -from ruamel import yaml from pathlib import Path logger = logging.getLogger('qBit Manage') @@ -62,26 +61,26 @@ def check_for_attribute(self, if data is None or attribute not in data or (attribute in data and data[attribute] is None and not default_is_none): message = f"{text} not found" if parent and save is True: - loaded_config, _, _ = yaml.util.load_yaml_guess_indent(open(self.config.config_path)) + yaml = YAML(self.config.config_path) if subparent: endline = f"\n{subparent} sub-attribute {attribute} added to config" - if subparent not in loaded_config[parent] or not loaded_config[parent][subparent]: - loaded_config[parent][subparent] = {attribute: default} - elif attribute not in loaded_config[parent]: - if isinstance(loaded_config[parent][subparent], str): - loaded_config[parent][subparent] = {attribute: default} - loaded_config[parent][subparent][attribute] = default + if subparent not in yaml.data[parent] or not yaml.data[parent][subparent]: + yaml.data[parent][subparent] = {attribute: default} + elif attribute not in yaml.data[parent]: + if isinstance(yaml.data[parent][subparent], str): + yaml.data[parent][subparent] = {attribute: default} + yaml.data[parent][subparent][attribute] = default else: endline = "" else: endline = f"\n{parent} sub-attribute {attribute} added to config" - if parent not in loaded_config or not loaded_config[parent]: - loaded_config[parent] = {attribute: default} - elif attribute not in loaded_config[parent] or (attribute in loaded_config[parent] and loaded_config[parent][attribute] is None): - loaded_config[parent][attribute] = default + if parent not in yaml.data or not yaml.data[parent]: + yaml.data[parent] = {attribute: default} + elif attribute not in yaml.data[parent] or (attribute in yaml.data[parent] and yaml.data[parent][attribute] is None): + yaml.data[parent][attribute] = default else: endline = "" - yaml.round_trip_dump(loaded_config, open(self.config.config_path, "w"), indent=None, block_seq_indent=2) + yaml.save() if default_is_none and var_type in ["list", "int_list"]: return [] elif data[attribute] is None: if default_is_none and var_type == "list": @@ -402,3 +401,36 @@ def human_readable_size(size, decimal_places=3): break size /= 1024.0 return f"{size:.{decimal_places}f}{unit}" + + +class YAML: + def __init__(self, path=None, input_data=None, check_empty=False, create=False): + self.path = path + self.input_data = input_data + self.yaml = ruamel.yaml.YAML() + self.yaml.indent(mapping=2, sequence=2) + try: + if input_data: + self.data = self.yaml.load(input_data) + else: + if create and not os.path.exists(self.path): + with open(self.path, 'w'): + pass + self.data = {} + else: + with open(self.path, encoding="utf-8") as fp: + self.data = self.yaml.load(fp) + except ruamel.yaml.error.YAMLError as e: + e = str(e).replace("\n", "\n ") + raise Failed(f"YAML Error: {e}") + except Exception as e: + raise Failed(f"YAML Error: {e}") + if not self.data or not isinstance(self.data, dict): + if check_empty: + raise Failed("YAML Error: File is empty") + self.data = {} + + def save(self): + if self.path: + with open(self.path, 'w') as fp: + self.yaml.dump(self.data, fp) From 659ffb7c045fd14e0695f6a4321ab553e33effeb Mon Sep 17 00:00:00 2001 From: bobokun Date: Sun, 14 Aug 2022 15:18:02 -0400 Subject: [PATCH 04/11] fix script error message --- scripts/mover.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/mover.py b/scripts/mover.py index dd94138e..6b713fd8 100644 --- a/scripts/mover.py +++ b/scripts/mover.py @@ -16,7 +16,7 @@ try: from qbittorrentapi import Client, LoginFailed, APIConnectionError except ModuleNotFoundError: - print("Requirements Error: qbittorrentapi not installed. Please install with pip") + print("Requirements Error: qbittorrent-api not installed. Please install using the command \"pip install qbittorrent-api\"") sys.exit(0) current = datetime.now() From 5a50d8b6947cd28c0522827f0373e87e11c2137a Mon Sep 17 00:00:00 2001 From: bobokun Date: Sun, 14 Aug 2022 21:41:33 -0400 Subject: [PATCH 05/11] Ability to load multiple config files #140 --- modules/apprise.py | 4 ++-- modules/bhd.py | 4 ++-- modules/config.py | 4 ++-- modules/notifiarr.py | 4 ++-- modules/qbittorrent.py | 4 ++-- modules/webhooks.py | 5 ++--- qbit_manage.py | 38 ++++++++++++++++++++++++++++---------- 7 files changed, 40 insertions(+), 23 deletions(-) diff --git a/modules/apprise.py b/modules/apprise.py index dfcc9995..d1f8d4d7 100644 --- a/modules/apprise.py +++ b/modules/apprise.py @@ -1,6 +1,6 @@ -import logging +from modules import util from modules.util import Failed -logger = logging.getLogger("qBit Manage") +logger = util.logger class Apprise: diff --git a/modules/bhd.py b/modules/bhd.py index f5b63b25..561f51b6 100644 --- a/modules/bhd.py +++ b/modules/bhd.py @@ -1,8 +1,8 @@ -import logging +from modules import util from modules.util import Failed from json import JSONDecodeError -logger = logging.getLogger("qBit Manage") +logger = util.logger base_url = "https://beyond-hd.me/api/" diff --git a/modules/config.py b/modules/config.py index 6612dc87..eafed41d 100644 --- a/modules/config.py +++ b/modules/config.py @@ -1,4 +1,4 @@ -import logging, os, requests, stat, time, re +import os, requests, stat, time, re from modules import util from modules.util import Failed, check, YAML from modules.qbittorrent import Qbt @@ -8,7 +8,7 @@ from modules.apprise import Apprise from retrying import retry -logger = logging.getLogger("qBit Manage") +logger = util.logger class Config: diff --git a/modules/notifiarr.py b/modules/notifiarr.py index 3dd143d5..3250b37c 100644 --- a/modules/notifiarr.py +++ b/modules/notifiarr.py @@ -1,9 +1,9 @@ -import logging +from modules import util from modules.util import Failed from json import JSONDecodeError -logger = logging.getLogger("qBit Manage") +logger = util.logger base_url = "https://notifiarr.com/api/v1/" dev_url = "https://dev.notifiarr.com/api/v1/" diff --git a/modules/qbittorrent.py b/modules/qbittorrent.py index 2648b778..e6d6927d 100644 --- a/modules/qbittorrent.py +++ b/modules/qbittorrent.py @@ -1,4 +1,4 @@ -import logging, os, sys +import os, sys from qbittorrentapi import Client, Version, LoginFailed, APIConnectionError, NotFound404Error, Conflict409Error from modules import util from modules.util import Failed, print_line, print_multiline, separator, list_in_text @@ -7,7 +7,7 @@ from fnmatch import fnmatch from alive_progress import alive_it, config_handler -logger = logging.getLogger("qBit Manage") +logger = util.logger class Qbt: diff --git a/modules/webhooks.py b/modules/webhooks.py index ddb58036..acdcc954 100644 --- a/modules/webhooks.py +++ b/modules/webhooks.py @@ -1,9 +1,8 @@ -import logging from json import JSONDecodeError - +from modules import util from modules.util import Failed -logger = logging.getLogger("qBit Manage") +logger = util.logger class Webhooks: diff --git a/qbit_manage.py b/qbit_manage.py index 42bd9428..1d6fc898 100644 --- a/qbit_manage.py +++ b/qbit_manage.py @@ -1,6 +1,6 @@ #!/usr/bin/python3 -import argparse, logging, os, sys, time +import argparse, logging, os, sys, time, glob from logging.handlers import RotatingFileHandler from datetime import datetime, timedelta @@ -25,9 +25,9 @@ parser.add_argument('-r', '--run', dest='run', action='store_true', default=False, help='Run without the scheduler. Script will exit after completion.') parser.add_argument('-sch', '--schedule', dest='min', default='1440', type=str, help='Schedule to run every x minutes. (Default set to 1440 (1 day))') parser.add_argument('-sd', '--startup-delay', dest='startupDelay', default='0', type=str, help='Set delay in seconds on the first run of a schedule (Default set to 0)') -parser.add_argument('-c', '--config-file', dest='configfile', action='store', default='config.yml', type=str, - help='This is used if you want to use a different name for your config.yml. Example: tv.yml') -parser.add_argument('-lf', '--log-file', dest='logfile', action='store', default='activity.log', type=str, help='This is used if you want to use a different name for your log file. Example: tv.log',) +parser.add_argument('-c', '--config-file', dest='configfiles', action='store', default='config.yml', type=str, + help='This is used if you want to use a different name for your config.yml or if you want to load multiple config files using *. Example: tv.yml or config*.yml') +parser.add_argument('-lf', '--log-file', dest='logfile', action='store', default='config.log', type=str, help='This is used if you want to use a different name for your log file. Example: tv.log',) parser.add_argument('-cs', '--cross-seed', dest='cross_seed', action="store_true", default=False, help='Use this after running cross-seed script to add torrents from the cross-seed output folder to qBittorrent') parser.add_argument('-re', '--recheck', dest='recheck', action="store_true", default=False, help='Recheck paused torrents sorted by lowest size. Resume if Completed.') @@ -72,7 +72,7 @@ def get_arg(env_str, default, arg_bool=False, arg_int=False): run = get_arg("QBT_RUN", args.run, arg_bool=True) sch = get_arg("QBT_SCHEDULE", args.min) startupDelay = get_arg("QBT_STARTUP_DELAY", args.startupDelay) -config_file = get_arg("QBT_CONFIG", args.configfile) +config_files = get_arg("QBT_CONFIG", args.configfiles) log_file = get_arg("QBT_LOGFILE", args.logfile) cross_seed = get_arg("QBT_CROSS_SEED", args.cross_seed, arg_bool=True) recheck = get_arg("QBT_RECHECK", args.recheck, arg_bool=True) @@ -95,16 +95,28 @@ def get_arg(env_str, default, arg_bool=False, arg_int=False): stats = {} args = {} -if os.path.isdir('/config') and os.path.exists(os.path.join('/config', config_file)): +if os.path.isdir('/config') and glob.glob(os.path.join('/config', config_files)): default_dir = '/config' else: default_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config") + +if '*' not in config_files: + config_files = [config_files] +else: + glob_configs = glob.glob(os.path.join(default_dir, config_files)) + if glob_configs: + config_files = [os.path.split(x)[-1] for x in glob_configs] + else: + print(f"Config Error: Unable to find any config files in the pattern '{config_files}'.") + sys.exit(0) + + for v in [ 'run', 'sch', 'startupDelay', - 'config_file', + 'config_files', 'log_file', 'cross_seed', 'recheck', @@ -190,6 +202,12 @@ def fmt_filter(record): logger.debug(f"Logs are saved in {file_logger}") +def start_loop(): + for config_file in config_files: + args["config_file"] = config_file + start() + + def start(): start_time = datetime.now() args["time"] = start_time.strftime("%H:%M") @@ -363,7 +381,7 @@ def calc_next_run(sch, print=False): logger.debug(f" --run (QBT_RUN): {run}") logger.debug(f" --schedule (QBT_SCHEDULE): {sch}") logger.debug(f" --startup-delay (QBT_STARTUP_DELAY): {startupDelay}") - logger.debug(f" --config-file (QBT_CONFIG): {config_file}") + logger.debug(f" --config-file (QBT_CONFIG): {config_files}") logger.debug(f" --log-file (QBT_LOGFILE): {log_file}") logger.debug(f" --cross-seed (QBT_CROSS_SEED): {cross_seed}") logger.debug(f" --recheck (QBT_RECHECK): {recheck}") @@ -384,7 +402,7 @@ def calc_next_run(sch, print=False): try: if run: logger.info(" Run Mode: Script will exit after completion.") - start() + start_loop() else: schedule.every(sch).minutes.do(start) time_str, _ = calc_next_run(sch) @@ -392,7 +410,7 @@ def calc_next_run(sch, print=False): if startupDelay: logger.info(f" Startup Delay: Initial Run will start after {startupDelay} seconds") time.sleep(startupDelay) - start() + start_loop() while not killer.kill_now: schedule.run_pending() time.sleep(60) From ffdcce183c4dcf1cf61eee3337921a8cb715ddb9 Mon Sep 17 00:00:00 2001 From: bobokun Date: Sat, 20 Aug 2022 22:14:28 -0400 Subject: [PATCH 06/11] Logging overhall, separate logs per config --- .flake8 | 7 +- modules/config.py | 14 +-- modules/logs.py | 256 +++++++++++++++++++++++++++++++++++++++++ modules/qbittorrent.py | 218 +++++++++++++++++------------------ modules/util.py | 103 +---------------- qbit_manage.py | 129 ++++++++++----------- 6 files changed, 440 insertions(+), 287 deletions(-) create mode 100644 modules/logs.py diff --git a/.flake8 b/.flake8 index 3b7e4bff..3270ef7c 100644 --- a/.flake8 +++ b/.flake8 @@ -6,7 +6,8 @@ ignore = E701, # E701 Multiple statements on one line (colon) E241, # E241 Multiple spaces after ',' E272, # E272 Multiple spaces before keyword - C901 # C901 Function is too complex - E722 # E722 Do not use bare except, specify exception instead - W503 # W503 Line break occurred before a binary operator + C901, # C901 Function is too complex + E722, # E722 Do not use bare except, specify exception instead + W503, # W503 Line break occurred before a binary operator + E402, # E402 module level import not at top of file max-line-length = 200 \ No newline at end of file diff --git a/modules/config.py b/modules/config.py index eafed41d..8d41788e 100644 --- a/modules/config.py +++ b/modules/config.py @@ -131,7 +131,7 @@ def hooks(attr): try: self.Webhooks.start_time_hooks(self.start_time) except Failed as e: - util.print_stacktrace() + logger.stacktrace() logger.error(f"Webhooks Error: {e}") self.BeyondHD = None @@ -350,11 +350,11 @@ def empty_recycle(self): recycle_files = sorted(recycle_files) if recycle_files: body = [] - util.separator(f"Emptying Recycle Bin (Files > {self.recyclebin['empty_after_x_days']} days)", space=True, border=True) + logger.separator(f"Emptying Recycle Bin (Files > {self.recyclebin['empty_after_x_days']} days)", space=True, border=True) prevfolder = '' for file in recycle_files: folder = re.search(f".*{os.path.basename(self.recycle_dir.rstrip(os.sep))}", file).group(0) - if folder != prevfolder: body += util.separator(f"Searching: {folder}", space=False, border=False) + if folder != prevfolder: body += logger.separator(f"Searching: {folder}", space=False, border=False) fileStats = os.stat(file) filename = os.path.basename(file) last_modified = fileStats[stat.ST_MTIME] # in seconds (last modified time) @@ -362,7 +362,7 @@ def empty_recycle(self): days = (now - last_modified) / (60 * 60 * 24) if (self.recyclebin['empty_after_x_days'] <= days): num_del += 1 - body += util.print_line(f"{'Did not delete' if dry_run else 'Deleted'} {filename} from {folder} (Last modified {round(days)} days ago).", loglevel) + body += logger.print_line(f"{'Did not delete' if dry_run else 'Deleted'} {filename} from {folder} (Last modified {round(days)} days ago).", loglevel) files += [str(filename)] size_bytes += os.path.getsize(file) if not dry_run: os.remove(file) @@ -371,7 +371,7 @@ def empty_recycle(self): if not dry_run: for path in recycle_path: util.remove_empty_directories(path, "**/*") - body += util.print_line(f"{'Did not delete' if dry_run else 'Deleted'} {num_del} files ({util.human_readable_size(size_bytes)}) from the Recycle Bin.", loglevel) + body += logger.print_line(f"{'Did not delete' if dry_run else 'Deleted'} {num_del} files ({util.human_readable_size(size_bytes)}) from the Recycle Bin.", loglevel) attr = { "function": "empty_recyclebin", "title": f"Emptying Recycle Bin (Files > {self.recyclebin['empty_after_x_days']} days)", @@ -397,7 +397,7 @@ def send_notifications(self, attr): if config_function: self.Webhooks.function_hooks([config_webhooks[config_function]], attr) except Failed as e: - util.print_stacktrace() + logger.stacktrace() logger.error(f"Webhooks Error: {e}") def notify(self, text, function=None, critical=True): @@ -405,7 +405,7 @@ def notify(self, text, function=None, critical=True): try: self.Webhooks.error_hooks(error, function_error=function, critical=critical) except Failed as e: - util.print_stacktrace() + logger.stacktrace() logger.error(f"Webhooks Error: {e}") def get_json(self, url, json=None, headers=None, params=None): diff --git a/modules/logs.py b/modules/logs.py new file mode 100644 index 00000000..ad73c77a --- /dev/null +++ b/modules/logs.py @@ -0,0 +1,256 @@ +import io, logging, os, re, sys, traceback +from logging.handlers import RotatingFileHandler + +LOG_DIR = "logs" + +CRITICAL = 50 +FATAL = CRITICAL +ERROR = 40 +WARNING = 30 +WARN = WARNING +DRYRUN = 25 +INFO = 20 +DEBUG = 10 + + +def fmt_filter(record): + record.levelname = f"[{record.levelname}]" + record.filename = f"[{record.filename}:{record.lineno}]" + return True + + +_srcfile = os.path.normcase(fmt_filter.__code__.co_filename) + + +class MyLogger: + def __init__(self, logger_name, log_file, log_level, default_dir, screen_width, separating_character, ignore_ghost, is_debug): + self.logger_name = logger_name + self.default_dir = default_dir + self.screen_width = screen_width + self.separating_character = separating_character + self.is_debug = is_debug + self.ignore_ghost = ignore_ghost + self.log_dir = os.path.join(default_dir, LOG_DIR) + self.main_log = log_file if os.path.exists(os.path.dirname(log_file)) else os.path.join(self.log_dir, log_file) + self.main_handler = None + self.save_errors = False + self.saved_errors = [] + self.config_handlers = {} + self.secrets = [] + self.spacing = 0 + os.makedirs(self.log_dir, exist_ok=True) + self._logger = logging.getLogger(self.logger_name) + logging.DRYRUN = DRYRUN + logging.addLevelName(DRYRUN, 'DRYRUN') + setattr(self._logger, 'dryrun', lambda dryrun, *args: self._logger._log(DRYRUN, dryrun, args)) + self._log_level = getattr(logging, log_level.upper()) + self._logger.setLevel(self._log_level) + + cmd_handler = logging.StreamHandler() + cmd_handler.setLevel(self._log_level) + + self._logger.addHandler(cmd_handler) + + def clear_errors(self): + self.saved_errors = [] + + def _get_handler(self, log_file, count=3): + max_bytes = 1024 * 1024 * 2 + _handler = RotatingFileHandler(log_file, delay=True, mode="w", maxBytes=max_bytes, backupCount=count, encoding="utf-8") + self._formatter(_handler) + # if os.path.isfile(log_file): + # _handler.doRollover() + return _handler + + def _formatter(self, handler, border=True): + text = f"| %(message)-{self.screen_width - 2}s |" if border else f"%(message)-{self.screen_width - 2}s" + if isinstance(handler, RotatingFileHandler): + text = f"[%(asctime)s] %(filename)-27s %(levelname)-10s {text}" + handler.setFormatter(logging.Formatter(text)) + + def add_main_handler(self): + self.main_handler = self._get_handler(self.main_log, count=9) + self.main_handler.addFilter(fmt_filter) + self._logger.addHandler(self.main_handler) + + def remove_main_handler(self): + self._logger.removeHandler(self.main_handler) + + def add_config_handler(self, config_key): + if config_key in self.config_handlers: + self._logger.addHandler(self.config_handlers[config_key]) + else: + self.config_handlers[config_key] = self._get_handler(os.path.join(self.log_dir, config_key + '.log')) + self._logger.addHandler(self.config_handlers[config_key]) + + def remove_config_handler(self, config_key): + if config_key in self.config_handlers: + self._logger.removeHandler(self.config_handlers[config_key]) + + def _centered(self, text, sep=" ", side_space=True, left=False): + if len(text) > self.screen_width - 2: + return text + space = self.screen_width - len(text) - 2 + text = f"{' ' if side_space else sep}{text}{' ' if side_space else sep}" + if space % 2 == 1: + text += sep + space -= 1 + side = int(space / 2) - 1 + final_text = f"{text}{sep * side}{sep * side}" if left else f"{sep * side}{text}{sep * side}" + return final_text + + def separator(self, text=None, space=True, border=True, side_space=True, left=False, loglevel='INFO'): + sep = " " if space else self.separating_character + for handler in self._logger.handlers: + self._formatter(handler, border=False) + border_text = f"|{self.separating_character * self.screen_width}|" + if border: + self.print_line(border_text, loglevel) + if text: + text_list = text.split("\n") + for t in text_list: + self.print_line(f"|{sep}{self._centered(t, sep=sep, side_space=side_space, left=left)}{sep}|", loglevel) + if border: + self.print_line(border_text, loglevel) + for handler in self._logger.handlers: + self._formatter(handler) + return [text] + + def print_line(self, msg, loglevel='INFO', *args, **kwargs): + loglvl = getattr(logging, loglevel.upper()) + if self._logger.isEnabledFor(loglvl): + self._log(loglvl, str(msg), args, **kwargs) + return [str(msg)] + + def debug(self, msg, *args, **kwargs): + if self._logger.isEnabledFor(DEBUG): + self._log(DEBUG, str(msg), args, **kwargs) + + def info_center(self, msg, *args, **kwargs): + self.info(self._centered(str(msg)), *args, **kwargs) + + def info(self, msg, *args, **kwargs): + if self._logger.isEnabledFor(INFO): + self._log(INFO, str(msg), args, **kwargs) + + def dryrun(self, msg, *args, **kwargs): + if self._logger.isEnabledFor(DRYRUN): + self._log(DRYRUN, str(msg), args, **kwargs) + + def warning(self, msg, *args, **kwargs): + if self._logger.isEnabledFor(WARNING): + self._log(WARNING, str(msg), args, **kwargs) + + def error(self, msg, *args, **kwargs): + if self.save_errors: + self.saved_errors.append(msg) + if self._logger.isEnabledFor(ERROR): + self._log(ERROR, str(msg), args, **kwargs) + + def critical(self, msg, *args, **kwargs): + if self.save_errors: + self.saved_errors.append(msg) + if self._logger.isEnabledFor(CRITICAL): + self._log(CRITICAL, str(msg), args, **kwargs) + + def stacktrace(self): + self.debug(traceback.format_exc()) + + def _space(self, display_title): + display_title = str(display_title) + space_length = self.spacing - len(display_title) + if space_length > 0: + display_title += " " * space_length + return display_title + + def ghost(self, text): + if not self.ignore_ghost: + try: + final_text = f"| {text}" + except UnicodeEncodeError: + text = text.encode("utf-8") + final_text = f"| {text}" + print(self._space(final_text), end="\r") + self.spacing = len(text) + 2 + + def exorcise(self): + if not self.ignore_ghost: + print(self._space(" "), end="\r") + self.spacing = 0 + + def secret(self, text): + if str(text) not in self.secrets: + self.secrets.append(str(text)) + + def insert_space(self, display_title, space_length=0): + display_title = str(display_title) + if space_length == 0: + space_length = self.spacing - len(display_title) + if space_length > 0: + display_title = " " * space_length + display_title + return display_title + + def _log(self, level, msg, args, exc_info=None, extra=None, stack_info=False, stacklevel=1): + if self.spacing > 0: + self.exorcise() + if "\n" in msg: + for i, line in enumerate(msg.split("\n")): + self._log(level, line, args, exc_info=exc_info, extra=extra, stack_info=stack_info, stacklevel=stacklevel) + if i == 0: + for handler in self._logger.handlers: + if isinstance(handler, RotatingFileHandler): + handler.setFormatter(logging.Formatter(" " * 65 + "| %(message)s")) + for handler in self._logger.handlers: + if isinstance(handler, RotatingFileHandler): + handler.setFormatter(logging.Formatter("[%(asctime)s] %(filename)-27s %(levelname)-10s | %(message)s")) + else: + for secret in self.secrets: + if secret in msg: + msg = msg.replace(secret, "(redacted)") + if "HTTPConnectionPool" in msg: + msg = re.sub("HTTPConnectionPool\\((.*?)\\)", "HTTPConnectionPool(redacted)", msg) + if "HTTPSConnectionPool" in msg: + msg = re.sub("HTTPSConnectionPool\\((.*?)\\)", "HTTPSConnectionPool(redacted)", msg) + try: + if not _srcfile: + raise ValueError + fn, lno, func, sinfo = self.findCaller(stack_info, stacklevel) + except ValueError: + fn, lno, func, sinfo = "(unknown file)", 0, "(unknown function)", None + if exc_info: + if isinstance(exc_info, BaseException): + exc_info = (type(exc_info), exc_info, exc_info.__traceback__) + elif not isinstance(exc_info, tuple): + exc_info = sys.exc_info() + record = self._logger.makeRecord(self._logger.name, level, fn, lno, msg, args, exc_info, func, extra, sinfo) + self._logger.handle(record) + + def findCaller(self, stack_info=False, stacklevel=1): + f = logging.currentframe() + if f is not None: + f = f.f_back + orig_f = f + while f and stacklevel > 1: + f = f.f_back + stacklevel -= 1 + if not f: + f = orig_f + rv = "(unknown file)", 0, "(unknown function)", None + while hasattr(f, "f_code"): + co = f.f_code + filename = os.path.normcase(co.co_filename) + if filename == _srcfile: + f = f.f_back + continue + sinfo = None + if stack_info: + sio = io.StringIO() + sio.write('Stack (most recent call last):\n') + traceback.print_stack(f, file=sio) + sinfo = sio.getvalue() + if sinfo[-1] == '\n': + sinfo = sinfo[:-1] + sio.close() + rv = (co.co_filename, f.f_lineno, co.co_name, sinfo) + break + return rv diff --git a/modules/qbittorrent.py b/modules/qbittorrent.py index e6d6927d..f002dd9c 100644 --- a/modules/qbittorrent.py +++ b/modules/qbittorrent.py @@ -1,7 +1,7 @@ import os, sys from qbittorrentapi import Client, Version, LoginFailed, APIConnectionError, NotFound404Error, Conflict409Error from modules import util -from modules.util import Failed, print_line, print_multiline, separator, list_in_text +from modules.util import Failed, list_in_text from datetime import timedelta from collections import Counter from fnmatch import fnmatch @@ -32,7 +32,7 @@ def __init__(self, config, params): e = (f"Qbittorrent Error: qbit_manage is only comaptible with {SUPPORTED_VERSION} or lower. You are currently on {CURRENT_VERSION}." + '\n' + f"Please downgrade to your Qbittorrent version to {SUPPORTED_VERSION} to use qbit_manage.") self.config.notify(e, "Qbittorrent") - print_multiline(e, 'CRITICAL') + logger.print_line(e, 'CRITICAL') sys.exit(0) logger.info("Qbt Connection Successful") except LoginFailed: @@ -47,7 +47,7 @@ def __init__(self, config, params): e = "Qbittorrent Error: Unable to connect to the client." self.config.notify(e, "Qbittorrent") raise Failed(e) - separator("Getting Torrent List", space=False, border=False) + logger.separator("Getting Torrent List", space=False, border=False) self.torrent_list = self.get_torrents({'sort': 'added_on'}) # Will create a 2D Dictionary with the torrent name as the key @@ -74,10 +74,10 @@ def get_torrent_info(torrent_list): t_obj_valid = [] t_obj_list = [] settings = self.config.settings - separator("Checking Settings", space=False, border=False) + logger.separator("Checking Settings", space=False, border=False) if settings['force_auto_tmm']: - print_line('force_auto_tmm set to True. Will force Auto Torrent Management for all torrents.', loglevel) - separator("Gathering Torrent Information", space=True, border=True) + logger.print_line('force_auto_tmm set to True. Will force Auto Torrent Management for all torrents.', loglevel) + logger.separator("Gathering Torrent Information", space=True, border=True) for torrent in alive_it(torrent_list): is_complete = False msg = None @@ -172,19 +172,19 @@ def update_cat(new_cat, cat_change): if torrent.auto_tmm is False and self.config.settings['force_auto_tmm']: torrent.set_auto_management(True) except Conflict409Error: - e = print_line(f'Existing category "{new_cat}" not found for save path {torrent.save_path}, category will be created.', loglevel) + e = logger.print_line(f'Existing category "{new_cat}" not found for save path {torrent.save_path}, category will be created.', loglevel) self.config.notify(e, 'Update Category', False) self.client.torrent_categories.create_category(name=new_cat, save_path=torrent.save_path) torrent.set_category(category=new_cat) body = [] - body += print_line(util.insert_space(f'Torrent Name: {torrent.name}', 3), loglevel) + body += logger.print_line(logger.insert_space(f'Torrent Name: {torrent.name}', 3), loglevel) if cat_change: - body += print_line(util.insert_space(f'Old Category: {old_cat}', 3), loglevel) + body += logger.print_line(logger.insert_space(f'Old Category: {old_cat}', 3), loglevel) title = "Moving Categories" else: title = "Updating Categories" - body += print_line(util.insert_space(f'New Category: {new_cat}', 3), loglevel) - body += print_line(util.insert_space(f'Tracker: {tracker["url"]}', 8), loglevel) + body += logger.print_line(logger.insert_space(f'New Category: {new_cat}', 3), loglevel) + body += logger.print_line(logger.insert_space(f'Tracker: {tracker["url"]}', 8), loglevel) attr = { "function": "cat_update", "title": title, @@ -198,7 +198,7 @@ def update_cat(new_cat, cat_change): num_cat += 1 if self.config.args['cat_update']: - separator("Updating Categories", space=False, border=False) + logger.separator("Updating Categories", space=False, border=False) torrent_list = self.get_torrents({'category': '', 'filter': 'completed'}) for torrent in torrent_list: new_cat = self.config.get_category(torrent.save_path) @@ -213,9 +213,9 @@ def update_cat(new_cat, cat_change): update_cat(new_cat, True) if num_cat >= 1: - print_line(f"{'Did not update' if dry_run else 'Updated'} {num_cat} new categories.", loglevel) + logger.print_line(f"{'Did not update' if dry_run else 'Updated'} {num_cat} new categories.", loglevel) else: - print_line('No new torrents to categorize.', loglevel) + logger.print_line('No new torrents to categorize.', loglevel) return num_cat def tags(self): @@ -224,7 +224,7 @@ def tags(self): num_tags = 0 ignore_tags = self.config.settings['ignoreTags_OnUpdate'] if self.config.args['tag_update']: - separator("Updating Tags", space=False, border=False) + logger.separator("Updating Tags", space=False, border=False) for torrent in self.torrent_list: check_tags = util.get_list(torrent.tags) if torrent.tags == '' or (len([x for x in check_tags if x not in ignore_tags]) == 0): @@ -232,9 +232,9 @@ def tags(self): if tracker["tag"]: num_tags += len(tracker["tag"]) body = [] - body += print_line(util.insert_space(f'Torrent Name: {torrent.name}', 3), loglevel) - body += print_line(util.insert_space(f'New Tag{"s" if len(tracker["tag"]) > 1 else ""}: {", ".join(tracker["tag"])}', 8), loglevel) - body += print_line(util.insert_space(f'Tracker: {tracker["url"]}', 8), loglevel) + body += logger.print_line(logger.insert_space(f'Torrent Name: {torrent.name}', 3), loglevel) + body += logger.print_line(logger.insert_space(f'New Tag{"s" if len(tracker["tag"]) > 1 else ""}: {", ".join(tracker["tag"])}', 8), loglevel) + body += logger.print_line(logger.insert_space(f'Tracker: {tracker["url"]}', 8), loglevel) body.extend(self.set_tags_and_limits(torrent, tracker["max_ratio"], tracker["max_seeding_time"], tracker["limit_upload_speed"], tracker["tag"])) category = self.config.get_category(torrent.save_path) if torrent.category == '' else torrent.category attr = { @@ -252,9 +252,9 @@ def tags(self): } self.config.send_notifications(attr) if num_tags >= 1: - print_line(f"{'Did not update' if dry_run else 'Updated'} {num_tags} new tags.", loglevel) + logger.print_line(f"{'Did not update' if dry_run else 'Updated'} {num_tags} new tags.", loglevel) else: - print_line('No new torrents to tag.', loglevel) + logger.print_line('No new torrents to tag.', loglevel) return num_tags def set_tags_and_limits(self, torrent, max_ratio, max_seeding_time, limit_upload_speed=None, tags=None, restore=False): @@ -263,18 +263,18 @@ def set_tags_and_limits(self, torrent, max_ratio, max_seeding_time, limit_upload body = [] # Print Logs if limit_upload_speed: - if limit_upload_speed == -1: body += print_line(util.insert_space('Limit UL Speed: Infinity', 1), loglevel) - else: body += print_line(util.insert_space(f'Limit UL Speed: {limit_upload_speed} kB/s', 1), loglevel) + if limit_upload_speed == -1: body += logger.print_line(logger.insert_space('Limit UL Speed: Infinity', 1), loglevel) + else: body += logger.print_line(logger.insert_space(f'Limit UL Speed: {limit_upload_speed} kB/s', 1), loglevel) if max_ratio or max_seeding_time: - if (max_ratio == -2 or max_seeding_time == -2) and not restore: body += print_line(util.insert_space('Share Limit: Use Global Share Limit', 4), loglevel) - elif (max_ratio == -1 or max_seeding_time == -1) and not restore: body += print_line(util.insert_space('Share Limit: Set No Share Limit', 4), loglevel) + if (max_ratio == -2 or max_seeding_time == -2) and not restore: body += logger.print_line(logger.insert_space('Share Limit: Use Global Share Limit', 4), loglevel) + elif (max_ratio == -1 or max_seeding_time == -1) and not restore: body += logger.print_line(logger.insert_space('Share Limit: Set No Share Limit', 4), loglevel) else: if max_ratio != torrent.max_ratio and (not max_seeding_time or max_seeding_time < 0): - body += print_line(util.insert_space(f'Share Limit: Max Ratio = {max_ratio}', 4), loglevel) + body += logger.print_line(logger.insert_space(f'Share Limit: Max Ratio = {max_ratio}', 4), loglevel) elif max_seeding_time != torrent.max_seeding_time and (not max_ratio or max_ratio < 0): - body += print_line(util.insert_space(f'Share Limit: Max Seed Time = {max_seeding_time} min', 4), loglevel) + body += logger.print_line(logger.insert_space(f'Share Limit: Max Seed Time = {max_seeding_time} min', 4), loglevel) elif max_ratio != torrent.max_ratio and max_seeding_time != torrent.max_seeding_time: - body += print_line(util.insert_space(f'Share Limit: Max Ratio = {max_ratio}, Max Seed Time = {max_seeding_time} min', 4), loglevel) + body += logger.print_line(logger.insert_space(f'Share Limit: Max Ratio = {max_ratio}, Max Seed Time = {max_seeding_time} min', 4), loglevel) # Update Torrents if not dry_run: if tags: torrent.add_tags(tags) @@ -302,7 +302,7 @@ def tag_nohardlinks(self): num_untag = 0 # counter for number of torrents that previously had no hard links but now have hard links if self.config.args['tag_nohardlinks']: - util.separator("Tagging Torrents with No Hardlinks", space=False, border=False) + logger.separator("Tagging Torrents with No Hardlinks", space=False, border=False) nohardlinks = self.config.nohardlinks tdel_dict = {} # dictionary to track the torrent names and content path that meet the deletion criteria root_dir = self.config.root_dir @@ -327,9 +327,9 @@ def tag_nohardlinks(self): if 'noHL' not in torrent.tags: num_tags += 1 body = [] - body += print_line(util.insert_space(f'Torrent Name: {torrent.name}', 3), loglevel) - body += print_line(util.insert_space('Added Tag: noHL', 6), loglevel) - body += print_line(util.insert_space(f'Tracker: {tracker["url"]}', 8), loglevel) + body += logger.print_line(logger.insert_space(f'Torrent Name: {torrent.name}', 3), loglevel) + body += logger.print_line(logger.insert_space('Added Tag: noHL', 6), loglevel) + body += logger.print_line(logger.insert_space(f'Tracker: {tracker["url"]}', 8), loglevel) body.extend(self.set_tags_and_limits(torrent, nohardlinks[category]["max_ratio"], nohardlinks[category]["max_seeding_time"], nohardlinks[category]["limit_upload_speed"], tags='noHL')) attr = { @@ -356,10 +356,10 @@ def tag_nohardlinks(self): if (not (util.nohardlink(torrent['content_path'].replace(root_dir, root_dir))) and ('noHL' in torrent.tags)): num_untag += 1 body = [] - body += print_line(f'Previous Tagged noHL Torrent Name: {torrent.name} has hard links found now.', loglevel) - body += print_line(util.insert_space('Removed Tag: noHL', 6), loglevel) - body += print_line(util.insert_space(f'Tracker: {tracker["url"]}', 8), loglevel) - body += print_line(f"{'Not Reverting' if dry_run else 'Reverting'} share limits.", loglevel) + body += logger.print_line(f'Previous Tagged noHL Torrent Name: {torrent.name} has hard links found now.', loglevel) + body += logger.print_line(logger.insert_space('Removed Tag: noHL', 6), loglevel) + body += logger.print_line(logger.insert_space(f'Tracker: {tracker["url"]}', 8), loglevel) + body += logger.print_line(f"{'Not Reverting' if dry_run else 'Reverting'} share limits.", loglevel) restore_max_ratio = tracker["max_ratio"] restore_max_seeding_time = tracker["max_seeding_time"] restore_limit_upload_speed = tracker["limit_upload_speed"] @@ -396,9 +396,9 @@ def tag_nohardlinks(self): if torrent['content_path'].replace(root_dir, root_dir) == tdel_dict[t_name]: tracker = self.config.get_tags([x.url for x in torrent.trackers if x.url.startswith('http')]) body = [] - body += print_line(util.insert_space(f'Torrent Name: {t_name}', 3), loglevel) - body += print_line(util.insert_space(f'Tracker: {tracker["url"]}', 8), loglevel) - body += print_line(util.insert_space("Cleanup: True [No hard links found and meets Share Limits.]", 8), loglevel) + body += logger.print_line(logger.insert_space(f'Torrent Name: {t_name}', 3), loglevel) + body += logger.print_line(logger.insert_space(f'Tracker: {tracker["url"]}', 8), loglevel) + body += logger.print_line(logger.insert_space("Cleanup: True [No hard links found and meets Share Limits.]", 8), loglevel) attr = { "function": "cleanup_tag_nohardlinks", "title": "Removing NoHL Torrents and meets Share Limits", @@ -414,27 +414,27 @@ def tag_nohardlinks(self): del_tor += 1 attr["torrents_deleted_and_contents"] = False if not dry_run: self.tor_delete_recycle(torrent, attr) - body += print_line(util.insert_space('Deleted .torrent but NOT content files.', 8), loglevel) + body += logger.print_line(logger.insert_space('Deleted .torrent but NOT content files.', 8), loglevel) else: del_tor_cont += 1 attr["torrents_deleted_and_contents"] = True if not dry_run: self.tor_delete_recycle(torrent, attr) - body += print_line(util.insert_space('Deleted .torrent AND content files.', 8), loglevel) + body += logger.print_line(logger.insert_space('Deleted .torrent AND content files.', 8), loglevel) else: del_tor += 1 attr["torrents_deleted_and_contents"] = False if not dry_run: self.tor_delete_recycle(torrent, attr) - body += print_line(util.insert_space('Deleted .torrent but NOT content files.', 8), loglevel) + body += logger.print_line(logger.insert_space('Deleted .torrent but NOT content files.', 8), loglevel) attr["body"] = "\n".join(body) self.config.send_notifications(attr) self.torrentinfo[t_name]['count'] -= 1 if num_tags >= 1: - print_line(f"{'Did not Tag/set' if dry_run else 'Tag/set'} share limits for {num_tags} .torrent{'s.' if num_tags > 1 else '.'}", loglevel) + logger.print_line(f"{'Did not Tag/set' if dry_run else 'Tag/set'} share limits for {num_tags} .torrent{'s.' if num_tags > 1 else '.'}", loglevel) else: - print_line('No torrents to tag with no hard links.', loglevel) - if num_untag >= 1: print_line(f"{'Did not delete' if dry_run else 'Deleted'} noHL tags / share limits for {num_untag} .torrent{'s.' if num_untag > 1 else '.'}", loglevel) - if del_tor >= 1: print_line(f"{'Did not delete' if dry_run else 'Deleted'} {del_tor} .torrent{'s' if del_tor > 1 else ''} but not content files.", loglevel) - if del_tor_cont >= 1: print_line(f"{'Did not delete' if dry_run else 'Deleted'} {del_tor_cont} .torrent{'s' if del_tor_cont > 1 else ''} AND content files.", loglevel) + logger.print_line('No torrents to tag with no hard links.', loglevel) + if num_untag >= 1: logger.print_line(f"{'Did not delete' if dry_run else 'Deleted'} noHL tags / share limits for {num_untag} .torrent{'s.' if num_untag > 1 else '.'}", loglevel) + if del_tor >= 1: logger.print_line(f"{'Did not delete' if dry_run else 'Deleted'} {del_tor} .torrent{'s' if del_tor > 1 else ''} but not content files.", loglevel) + if del_tor_cont >= 1: logger.print_line(f"{'Did not delete' if dry_run else 'Deleted'} {del_tor_cont} .torrent{'s' if del_tor_cont > 1 else ''} AND content files.", loglevel) return num_tags, num_untag, del_tor, del_tor_cont def rem_unregistered(self): @@ -452,10 +452,10 @@ def rem_unregistered(self): def tag_tracker_error(): nonlocal dry_run, t_name, msg_up, msg, tracker, t_cat, torrent, tag_error, tor_error_summary, num_tor_error tor_error = '' - tor_error += (util.insert_space(f'Torrent Name: {t_name}', 3)+'\n') - tor_error += (util.insert_space(f'Status: {msg}', 9)+'\n') - tor_error += (util.insert_space(f'Tracker: {tracker["url"]}', 8)+'\n') - tor_error += (util.insert_space(f"Added Tag: {tag_error}", 6)+'\n') + tor_error += (logger.insert_space(f'Torrent Name: {t_name}', 3)+'\n') + tor_error += (logger.insert_space(f'Status: {msg}', 9)+'\n') + tor_error += (logger.insert_space(f'Tracker: {tracker["url"]}', 8)+'\n') + tor_error += (logger.insert_space(f"Added Tag: {tag_error}", 6)+'\n') tor_error_summary += tor_error num_tor_error += 1 attr = { @@ -475,9 +475,9 @@ def tag_tracker_error(): def del_unregistered(): nonlocal dry_run, loglevel, del_tor, del_tor_cont, t_name, msg_up, msg, tracker, t_cat, t_msg, t_status, torrent body = [] - body += print_line(util.insert_space(f'Torrent Name: {t_name}', 3), loglevel) - body += print_line(util.insert_space(f'Status: {msg}', 9), loglevel) - body += print_line(util.insert_space(f'Tracker: {tracker["url"]}', 8), loglevel) + body += logger.print_line(logger.insert_space(f'Torrent Name: {t_name}', 3), loglevel) + body += logger.print_line(logger.insert_space(f'Status: {msg}', 9), loglevel) + body += logger.print_line(logger.insert_space(f'Tracker: {tracker["url"]}', 8), loglevel) attr = { "function": "rem_unregistered", "title": "Removing Unregistered Torrents", @@ -492,25 +492,25 @@ def del_unregistered(): if '' in t_msg or 2 in t_status: attr["torrents_deleted_and_contents"] = False if not dry_run: self.tor_delete_recycle(torrent, attr) - body += print_line(util.insert_space('Deleted .torrent but NOT content files.', 8), loglevel) + body += logger.print_line(logger.insert_space('Deleted .torrent but NOT content files.', 8), loglevel) del_tor += 1 else: attr["torrents_deleted_and_contents"] = True if not dry_run: self.tor_delete_recycle(torrent, attr) - body += print_line(util.insert_space('Deleted .torrent AND content files.', 8), loglevel) + body += logger.print_line(logger.insert_space('Deleted .torrent AND content files.', 8), loglevel) del_tor_cont += 1 else: attr["torrents_deleted_and_contents"] = True if not dry_run: self.tor_delete_recycle(torrent, attr) - body += print_line(util.insert_space('Deleted .torrent AND content files.', 8), loglevel) + body += logger.print_line(logger.insert_space('Deleted .torrent AND content files.', 8), loglevel) del_tor_cont += 1 attr["body"] = "\n".join(body) self.config.send_notifications(attr) self.torrentinfo[t_name]['count'] -= 1 if cfg_rem_unregistered or cfg_tag_error: - if cfg_tag_error: separator("Tagging Torrents with Tracker Errors", space=False, border=False) - elif cfg_rem_unregistered: separator("Removing Unregistered Torrents", space=False, border=False) + if cfg_tag_error: logger.separator("Tagging Torrents with Tracker Errors", space=False, border=False) + elif cfg_rem_unregistered: logger.separator("Removing Unregistered Torrents", space=False, border=False) unreg_msgs = [ 'UNREGISTERED', 'TORRENT NOT FOUND', @@ -540,10 +540,10 @@ def del_unregistered(): tracker = self.config.get_tags([x.url for x in torrent.trackers if x.url.startswith('http')]) num_untag += 1 body = [] - body += print_line(f'Previous Tagged {tag_error} torrent currently has a working tracker.', loglevel) - body += print_line(util.insert_space(f'Torrent Name: {torrent.name}', 3), loglevel) - body += print_line(util.insert_space(f'Removed Tag: {tag_error}', 4), loglevel) - body += print_line(util.insert_space(f'Tracker: {tracker["url"]}', 8), loglevel) + body += logger.print_line(f'Previous Tagged {tag_error} torrent currently has a working tracker.', loglevel) + body += logger.print_line(logger.insert_space(f'Torrent Name: {torrent.name}', 3), loglevel) + body += logger.print_line(logger.insert_space(f'Removed Tag: {tag_error}', 4), loglevel) + body += logger.print_line(logger.insert_space(f'Tracker: {tracker["url"]}', 8), loglevel) if not dry_run: torrent.remove_tags(tags=tag_error) attr = { "function": "untag_tracker_error", @@ -590,19 +590,19 @@ def del_unregistered(): except NotFound404Error: continue except Exception as e: - util.print_stacktrace() + logger.stacktrace() self.config.notify(e, 'Remove Unregistered Torrents', False) logger.error(f"Unknown Error: {e}") if cfg_rem_unregistered: if del_tor >= 1 or del_tor_cont >= 1: - if del_tor >= 1: print_line(f"{'Did not delete' if dry_run else 'Deleted'} {del_tor} .torrent{'s' if del_tor > 1 else ''} but not content files.", loglevel) - if del_tor_cont >= 1: print_line(f"{'Did not delete' if dry_run else 'Deleted'} {del_tor_cont} .torrent{'s' if del_tor_cont > 1 else ''} AND content files.", loglevel) + if del_tor >= 1: logger.print_line(f"{'Did not delete' if dry_run else 'Deleted'} {del_tor} .torrent{'s' if del_tor > 1 else ''} but not content files.", loglevel) + if del_tor_cont >= 1: logger.print_line(f"{'Did not delete' if dry_run else 'Deleted'} {del_tor_cont} .torrent{'s' if del_tor_cont > 1 else ''} AND content files.", loglevel) else: - print_line('No unregistered torrents found.', loglevel) - if num_untag >= 1: print_line(f"{'Did not delete' if dry_run else 'Deleted'} {tag_error} tags for {num_untag} .torrent{'s.' if num_untag > 1 else '.'}", loglevel) + logger.print_line('No unregistered torrents found.', loglevel) + if num_untag >= 1: logger.print_line(f"{'Did not delete' if dry_run else 'Deleted'} {tag_error} tags for {num_untag} .torrent{'s.' if num_untag > 1 else '.'}", loglevel) if num_tor_error >= 1: - separator(f"{num_tor_error} Torrents with tracker errors found", space=False, border=False, loglevel=loglevel) - print_multiline(tor_error_summary.rstrip(), loglevel) + logger.separator(f"{num_tor_error} Torrents with tracker errors found", space=False, border=False, loglevel=loglevel) + logger.print_line(tor_error_summary.rstrip(), loglevel) return del_tor, del_tor_cont, num_tor_error, num_untag # Function used to move any torrents from the cross seed directory to the correct save directory @@ -612,7 +612,7 @@ def cross_seed(self): added = 0 # Keep track of total torrents tagged tagged = 0 # Track # of torrents tagged that are not cross-seeded if self.config.args['cross_seed']: - separator("Checking for Cross-Seed Torrents", space=False, border=False) + logger.separator("Checking for Cross-Seed Torrents", space=False, border=False) # List of categories for all torrents moved categories = [] @@ -638,11 +638,11 @@ def cross_seed(self): if self.torrentinfo[t_name]['is_complete']: categories.append(category) body = [] - body += print_line(f"{'Not Adding' if dry_run else 'Adding'} to qBittorrent:", loglevel) - body += print_line(util.insert_space(f'Torrent Name: {t_name}', 3), loglevel) - body += print_line(util.insert_space(f'Category: {category}', 7), loglevel) - body += print_line(util.insert_space(f'Save_Path: {dest}', 6), loglevel) - body += print_line(util.insert_space(f'Tracker: {t_tracker}', 8), loglevel) + body += logger.print_line(f"{'Not Adding' if dry_run else 'Adding'} to qBittorrent:", loglevel) + body += logger.print_line(logger.insert_space(f'Torrent Name: {t_name}', 3), loglevel) + body += logger.print_line(logger.insert_space(f'Category: {category}', 7), loglevel) + body += logger.print_line(logger.insert_space(f'Save_Path: {dest}', 6), loglevel) + body += logger.print_line(logger.insert_space(f'Tracker: {t_tracker}', 8), loglevel) attr = { "function": "cross_seed", "title": "Adding New Cross-Seed Torrent", @@ -659,12 +659,12 @@ def cross_seed(self): self.client.torrents.add(torrent_files=src, save_path=dest, category=category, tags='cross-seed', is_paused=True) util.move_files(src, dir_cs_out) else: - print_line(f'Found {t_name} in {dir_cs} but original torrent is not complete.', loglevel) - print_line('Not adding to qBittorrent', loglevel) + logger.print_line(f'Found {t_name} in {dir_cs} but original torrent is not complete.', loglevel) + logger.print_line('Not adding to qBittorrent', loglevel) else: error = f'{t_name} not found in torrents. Cross-seed Torrent not added to qBittorrent.' - if dry_run: print_line(error, loglevel) - else: print_line(error, 'WARNING') + if dry_run: logger.print_line(error, loglevel) + else: logger.print_line(error, 'WARNING') self.config.notify(error, 'cross-seed', False) # Tag missing cross-seed torrents tags for torrent in self.torrent_list: @@ -673,7 +673,7 @@ def cross_seed(self): if 'cross-seed' not in torrent.tags and self.torrentinfo[t_name]['count'] > 1 and self.torrentinfo[t_name]['first_hash'] != torrent.hash: tracker = self.config.get_tags([x.url for x in torrent.trackers if x.url.startswith('http')]) tagged += 1 - body = print_line(f"{'Not Adding' if dry_run else 'Adding'} 'cross-seed' tag to {t_name}", loglevel) + body = logger.print_line(f"{'Not Adding' if dry_run else 'Adding'} 'cross-seed' tag to {t_name}", loglevel) attr = { "function": "tag_cross_seed", "title": "Tagging Cross-Seed Torrent", @@ -688,9 +688,9 @@ def cross_seed(self): numcategory = Counter(categories) for c in numcategory: - if numcategory[c] > 0: print_line(f"{numcategory[c]} {c} cross-seed .torrents {'not added' if dry_run else 'added'}.", loglevel) - if added > 0: print_line(f"Total {added} cross-seed .torrents {'not added' if dry_run else 'added'}.", loglevel) - if tagged > 0: print_line(f"Total {tagged} cross-seed .torrents {'not tagged' if dry_run else 'tagged'}.", loglevel) + if numcategory[c] > 0: logger.print_line(f"{numcategory[c]} {c} cross-seed .torrents {'not added' if dry_run else 'added'}.", loglevel) + if added > 0: logger.print_line(f"Total {added} cross-seed .torrents {'not added' if dry_run else 'added'}.", loglevel) + if tagged > 0: logger.print_line(f"Total {tagged} cross-seed .torrents {'not tagged' if dry_run else 'tagged'}.", loglevel) return added, tagged # Function used to recheck paused torrents sorted by size and resume torrents that are completed @@ -700,7 +700,7 @@ def recheck(self): resumed = 0 rechecked = 0 if self.config.args['recheck']: - separator("Rechecking Paused Torrents", space=False, border=False) + logger.separator("Rechecking Paused Torrents", space=False, border=False) # sort by size and paused torrent_list = self.get_torrents({'status_filter': 'paused', 'sort': 'size'}) if torrent_list: @@ -710,7 +710,7 @@ def recheck(self): if torrent.progress == 1: if torrent.max_ratio < 0 and torrent.max_seeding_time < 0: resumed += 1 - body = print_line(f"{'Not Resuming' if dry_run else 'Resuming'} [{tracker['tag']}] - {torrent.name}", loglevel) + body = logger.print_line(f"{'Not Resuming' if dry_run else 'Resuming'} [{tracker['tag']}] - {torrent.name}", loglevel) attr = { "function": "recheck", "title": "Resuming Torrent", @@ -725,14 +725,14 @@ def recheck(self): else: # Check to see if torrent meets AutoTorrentManagement criteria logger.debug('DEBUG: Torrent to see if torrent meets AutoTorrentManagement Criteria') - logger.debug(util.insert_space(f'- Torrent Name: {torrent.name}', 2)) - logger.debug(util.insert_space(f'-- Ratio vs Max Ratio: {torrent.ratio} < {torrent.max_ratio}', 4)) - logger.debug(util.insert_space(f'-- Seeding Time vs Max Seed Time: {timedelta(seconds=torrent.seeding_time)} < {timedelta(minutes=torrent.max_seeding_time)}', 4)) + logger.debug(logger.insert_space(f'- Torrent Name: {torrent.name}', 2)) + logger.debug(logger.insert_space(f'-- Ratio vs Max Ratio: {torrent.ratio} < {torrent.max_ratio}', 4)) + logger.debug(logger.insert_space(f'-- Seeding Time vs Max Seed Time: {timedelta(seconds=torrent.seeding_time)} < {timedelta(minutes=torrent.max_seeding_time)}', 4)) if (torrent.max_ratio >= 0 and torrent.ratio < torrent.max_ratio and torrent.max_seeding_time < 0) \ or (torrent.max_seeding_time >= 0 and (torrent.seeding_time < (torrent.max_seeding_time * 60)) and torrent.max_ratio < 0) \ or (torrent.max_ratio >= 0 and torrent.max_seeding_time >= 0 and torrent.ratio < torrent.max_ratio and (torrent.seeding_time < (torrent.max_seeding_time * 60))): resumed += 1 - body = print_line(f"{'Not Resuming' if dry_run else 'Resuming'} [{tracker['tag']}] - {torrent.name}", loglevel) + body = logger.print_line(f"{'Not Resuming' if dry_run else 'Resuming'} [{tracker['tag']}] - {torrent.name}", loglevel) attr = { "function": "recheck", "title": "Resuming Torrent", @@ -747,7 +747,7 @@ def recheck(self): # Recheck elif torrent.progress == 0 and self.torrentinfo[torrent.name]['is_complete'] and not torrent.state_enum.is_checking: rechecked += 1 - body = print_line(f"{'Not Rechecking' if dry_run else 'Rechecking'} [{tracker['tag']}] - {torrent.name}", loglevel) + body = logger.print_line(f"{'Not Rechecking' if dry_run else 'Rechecking'} [{tracker['tag']}] - {torrent.name}", loglevel) attr = { "function": "recheck", "title": "Rechecking Torrent", @@ -766,7 +766,7 @@ def rem_orphaned(self): loglevel = 'DRYRUN' if dry_run else 'INFO' orphaned = 0 if self.config.args['rem_orphaned']: - separator("Checking for Orphaned Files", space=False, border=False) + logger.separator("Checking for Orphaned Files", space=False, border=False) torrent_files = [] root_files = [] orphaned_files = [] @@ -800,24 +800,24 @@ def rem_orphaned(self): orphaned_files = set(orphaned_files) - set(excluded_orphan_files) if self.config.trace_mode: - separator("Torrent Files", space=False, border=False, loglevel='DEBUG') - print_multiline("\n".join(torrent_files), 'DEBUG') - separator("Root Files", space=False, border=False, loglevel='DEBUG') - print_multiline("\n".join(root_files), 'DEBUG') - separator("Excluded Orphan Files", space=False, border=False, loglevel='DEBUG') - print_multiline("\n".join(excluded_orphan_files), 'DEBUG') - separator("Orphaned Files", space=False, border=False, loglevel='DEBUG') - print_multiline("\n".join(orphaned_files), 'DEBUG') - separator("Deleting Orphaned Files", space=False, border=False, loglevel='DEBUG') + logger.separator("Torrent Files", space=False, border=False, loglevel='DEBUG') + logger.print_line("\n".join(torrent_files), 'DEBUG') + logger.separator("Root Files", space=False, border=False, loglevel='DEBUG') + logger.print_line("\n".join(root_files), 'DEBUG') + logger.separator("Excluded Orphan Files", space=False, border=False, loglevel='DEBUG') + logger.print_line("\n".join(excluded_orphan_files), 'DEBUG') + logger.separator("Orphaned Files", space=False, border=False, loglevel='DEBUG') + logger.print_line("\n".join(orphaned_files), 'DEBUG') + logger.separator("Deleting Orphaned Files", space=False, border=False, loglevel='DEBUG') if orphaned_files: dir_out = os.path.join(remote_path, 'orphaned_data') os.makedirs(dir_out, exist_ok=True) body = [] num_orphaned = len(orphaned_files) - print_line(f"{num_orphaned} Orphaned files found", loglevel) - body += print_multiline("\n".join(orphaned_files), loglevel) - body += print_line(f"{'Did not move' if dry_run else 'Moved'} {num_orphaned} Orphaned files to {dir_out.replace(remote_path,root_path)}", loglevel) + logger.print_line(f"{num_orphaned} Orphaned files found", loglevel) + body += logger.print_line("\n".join(orphaned_files), loglevel) + body += logger.print_line(f"{'Did not move' if dry_run else 'Moved'} {num_orphaned} Orphaned files to {dir_out.replace(remote_path,root_path)}", loglevel) attr = { "function": "rem_orphaned", @@ -839,7 +839,7 @@ def rem_orphaned(self): for parent_path in orphaned_parent_path: util.remove_empty_directories(parent_path, "**/*") else: - print_line("No Orphaned Files found.", loglevel) + logger.print_line("No Orphaned Files found.", loglevel) return orphaned def tor_delete_recycle(self, torrent, info): @@ -881,7 +881,7 @@ def tor_delete_recycle(self, torrent, info): try: util.copy_files(os.path.join(self.config.torrents_dir, File), os.path.join(torrent_path, File)) except Exception as e: - util.print_stacktrace() + logger.stacktrace() self.config.notify(e, 'Deleting Torrent', False) logger.warning(f"RecycleBin Warning: {e}") if "tracker_torrent_files" in torrent_json: @@ -909,9 +909,9 @@ def tor_delete_recycle(self, torrent, info): logger.debug(f"JSON: {torrent_json}") util.save_json(torrent_json, torrent_json_file) if info['torrents_deleted_and_contents'] is True: - separator(f"Moving {len(tor_files)} files to RecycleBin", space=False, border=False, loglevel='DEBUG') - if len(tor_files) == 1: print_line(tor_files[0], 'DEBUG') - else: print_multiline("\n".join(tor_files), 'DEBUG') + logger.separator(f"Moving {len(tor_files)} files to RecycleBin", space=False, border=False, loglevel='DEBUG') + if len(tor_files) == 1: logger.print_line(tor_files[0], 'DEBUG') + else: logger.print_line("\n".join(tor_files), 'DEBUG') logger.debug(f'Moved {len(tor_files)} files to {recycle_path.replace(self.config.remote_dir,self.config.root_dir)}') # Move files from torrent contents to Recycle bin @@ -922,7 +922,7 @@ def tor_delete_recycle(self, torrent, info): try: toDelete = util.move_files(src, dest, True) except FileNotFoundError: - e = print_line(f'RecycleBin Warning - FileNotFound: No such file or directory: {src} ', 'WARNING') + e = logger.print_line(f'RecycleBin Warning - FileNotFound: No such file or directory: {src} ', 'WARNING') self.config.notify(e, 'Deleting Torrent', False) # Delete torrent and files torrent.delete(delete_files=toDelete) diff --git a/modules/util.py b/modules/util.py index 2c60d191..ae10ea69 100644 --- a/modules/util.py +++ b/modules/util.py @@ -1,5 +1,4 @@ -import logging, os, shutil, traceback, time, signal, json, ruamel.yaml -from logging.handlers import RotatingFileHandler +import logging, os, shutil, time, signal, json, ruamel.yaml from pathlib import Path logger = logging.getLogger('qBit Manage') @@ -161,9 +160,9 @@ def check_for_attribute(self, message = message + "\n" + options raise Failed(f"Config Error: {message}") if do_print: - print_multiline(f"Config Warning: {message}", "warning") + logger.print(f"Config Warning: {message}", "warning") if data and attribute in data and data[attribute] and test_list is not None and data[attribute] not in test_list: - print_multiline(options) + logger.print(options) return default @@ -201,98 +200,6 @@ def list_in_text(text, search_list, match_all=False): return False -def print_line(lines, loglevel='INFO'): - logger.log(getattr(logging, loglevel.upper()), str(lines)) - return [str(lines)] - - -def print_multiline(lines, loglevel='INFO'): - for i, line in enumerate(str(lines).split("\n")): - logger.log(getattr(logging, loglevel.upper()), line) - if i == 0: - logger.handlers[1].setFormatter(logging.Formatter(" " * 65 + "| %(message)s")) - logger.handlers[1].setFormatter(logging.Formatter("[%(asctime)s] %(filename)-27s %(levelname)-10s | %(message)s")) - return [(str(lines))] - - -def print_stacktrace(): - print_multiline(traceback.format_exc(), 'CRITICAL') - - -def my_except_hook(exctype, value, tb): - for line in traceback.format_exception(etype=exctype, value=value, tb=tb): - print_multiline(line, 'CRITICAL') - - -def centered(text, sep=" "): - if len(text) > screen_width - 2: - return text - space = screen_width - len(text) - 2 - text = f" {text} " - if space % 2 == 1: - text += sep - space -= 1 - side = int(space / 2) - 1 - final_text = f"{sep * side}{text}{sep * side}" - return final_text - - -def separator(text=None, space=True, border=True, loglevel='INFO'): - sep = " " if space else separating_character - for handler in logger.handlers: - apply_formatter(handler, border=False) - border_text = f"|{separating_character * screen_width}|" - if border: - logger.log(getattr(logging, loglevel.upper()), border_text) - if text: - text_list = text.split("\n") - for t in text_list: - logger.log(getattr(logging, loglevel.upper()), - f"|{sep}{centered(t, sep=sep)}{sep}|") - if border: - logger.log(getattr(logging, loglevel.upper()), border_text) - for handler in logger.handlers: - apply_formatter(handler) - return [text] - - -def apply_formatter(handler, border=True): - text = f"| %(message)-{screen_width - 2}s |" if border else f"%(message)-{screen_width - 2}s" - if isinstance(handler, RotatingFileHandler): - text = f"[%(asctime)s] %(filename)-27s %(levelname)-10s {text}" - # text = f"[%(asctime)s] %(levelname)-10s {text}" - handler.setFormatter(logging.Formatter(text)) - - -def adjust_space(display_title): - display_title = str(display_title) - space_length = spacing - len(display_title) - if space_length > 0: - display_title += " " * space_length - return display_title - - -def insert_space(display_title, space_length=0): - display_title = str(display_title) - if space_length == 0: - space_length = spacing - len(display_title) - if space_length > 0: - display_title = " " * space_length + display_title - return display_title - - -def print_return(text): - print(adjust_space(f"| {text}"), end="\r") - global spacing - spacing = len(text) + 2 - - -def print_end(): - print(adjust_space(" "), end="\r") - global spacing - spacing = 0 - - # truncate the value of the torrent url to remove sensitive information def trunc_val(s, d, n=3): try: @@ -318,7 +225,7 @@ def move_files(src, dest, mod=False): shutil.copyfile(src, dest) toDelete = True except Exception as e: - print_stacktrace() + logger.stacktrace() logger.error(e) return toDelete @@ -331,7 +238,7 @@ def copy_files(src, dest): try: shutil.copyfile(src, dest) except Exception as e: - print_stacktrace() + logger.stacktrace() logger.error(e) diff --git a/qbit_manage.py b/qbit_manage.py index 1d6fc898..89ac7934 100644 --- a/qbit_manage.py +++ b/qbit_manage.py @@ -1,15 +1,11 @@ #!/usr/bin/python3 -import argparse, logging, os, sys, time, glob -from logging.handlers import RotatingFileHandler +import argparse, os, sys, time, glob from datetime import datetime, timedelta try: import schedule - from modules import util - from modules.config import Config - from modules.util import GracefulKiller - from modules.util import Failed + from modules.logs import MyLogger except ModuleNotFoundError: print("Requirements Error: Requirements are not installed") sys.exit(0) @@ -27,7 +23,8 @@ parser.add_argument('-sd', '--startup-delay', dest='startupDelay', default='0', type=str, help='Set delay in seconds on the first run of a schedule (Default set to 0)') parser.add_argument('-c', '--config-file', dest='configfiles', action='store', default='config.yml', type=str, help='This is used if you want to use a different name for your config.yml or if you want to load multiple config files using *. Example: tv.yml or config*.yml') -parser.add_argument('-lf', '--log-file', dest='logfile', action='store', default='config.log', type=str, help='This is used if you want to use a different name for your log file. Example: tv.log',) +parser.add_argument('-lf', '--log-file', dest='logfile', action='store', default='qbit_manage.log', type=str, + help='This is used if you want to use a different name for your log file. Example: tv.log',) parser.add_argument('-cs', '--cross-seed', dest='cross_seed', action="store_true", default=False, help='Use this after running cross-seed script to add torrents from the cross-seed output folder to qBittorrent') parser.add_argument('-re', '--recheck', dest='recheck', action="store_true", default=False, help='Recheck paused torrents sorted by lowest size. Resume if Completed.') @@ -52,19 +49,25 @@ def get_arg(env_str, default, arg_bool=False, arg_int=False): - env_var = os.environ.get(env_str) - if env_var: + env_vars = [env_str] if not isinstance(env_str, list) else env_str + final_value = None + for env_var in env_vars: + env_value = os.environ.get(env_var) + if env_value is not None: + final_value = env_value + break + if final_value is not None: if arg_bool: - if env_var is True or env_var is False: - return env_var - elif env_var.lower() in ["t", "true"]: + if final_value is True or final_value is False: + return final_value + elif final_value.lower() in ["t", "true"]: return True else: return False elif arg_int: - return int(env_var) + return int(final_value) else: - return str(env_var) + return str(final_value) else: return default @@ -136,12 +139,9 @@ def get_arg(env_str, default, arg_bool=False, arg_int=False): ]: args[v] = eval(v) -util.separating_character = divider[0] - if screen_width < 90 or screen_width > 300: print(f"Argument Error: width argument invalid: {screen_width} must be an integer between 90 and 300 using the default 100") screen_width = 100 -util.screen_width = screen_width # Check if Schedule parameter is a number try: @@ -157,25 +157,23 @@ def get_arg(env_str, default, arg_bool=False, arg_int=False): print(f"startupDelay Error: startupDelay is not a number. Current value is set to '{startupDelay}'") sys.exit(0) -logger = logging.getLogger('qBit Manage') -logging.DRYRUN = 25 -logging.addLevelName(logging.DRYRUN, 'DRYRUN') -setattr(logger, 'dryrun', lambda dryrun, *args: logger._log(logging.DRYRUN, dryrun, args)) -log_lev = getattr(logging, log_level.upper()) -logger.setLevel(log_lev) +logger = MyLogger('qBit Manage', log_file, log_level, default_dir, screen_width, divider[0], False, debug or trace) +from modules import util +util.logger = logger +from modules.config import Config +from modules.util import GracefulKiller +from modules.util import Failed -def fmt_filter(record): - record.levelname = f"[{record.levelname}]" - record.filename = f"[{record.filename}:{record.lineno}]" - return True +def my_except_hook(exctype, value, tb): + if issubclass(exctype, KeyboardInterrupt): + sys.__excepthook__(exctype, value, tb) + else: + logger.critical("Uncaught Exception", exc_info=(exctype, value, tb)) -cmd_handler = logging.StreamHandler() -cmd_handler.setLevel(log_level) -logger.addHandler(cmd_handler) -sys.excepthook = util.my_except_hook +sys.excepthook = my_except_hook version = "Unknown" with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), "VERSION")) as handle: @@ -185,27 +183,18 @@ def fmt_filter(record): version = line break -if os.path.exists(os.path.dirname(log_file)): - file_logger = log_file -elif not os.path.exists(os.path.dirname(log_file)) and os.path.dirname(log_file) != '': - os.makedirs(os.path.join(default_dir, 'logs'), exist_ok=True) - print(f"Log Warning: Log Path {os.path.dirname(log_file)} does not exist. Logs will be saved in the default path: {os.path.join(default_dir, 'logs', os.path.basename(log_file))}") - file_logger = os.path.join(default_dir, 'logs', os.path.basename(log_file)) -else: - os.makedirs(os.path.join(default_dir, 'logs'), exist_ok=True) - file_logger = os.path.join(default_dir, 'logs', os.path.basename(log_file)) -max_bytes = 1024 * 1024 * 2 -file_handler = RotatingFileHandler(file_logger, delay=True, mode="w", maxBytes=max_bytes, backupCount=10, encoding="utf-8") -util.apply_formatter(file_handler) -file_handler.addFilter(fmt_filter) -logger.addHandler(file_handler) -logger.debug(f"Logs are saved in {file_logger}") - def start_loop(): - for config_file in config_files: - args["config_file"] = config_file + if len(config_files) == 1: + args["config_file"] = config_files[0] start() + else: + for config_file in config_files: + args["config_file"] = config_file + config_base = os.path.splitext(config_file)[0] + logger.add_config_handler(config_base) + start() + logger.remove_config_handler(config_base) def start(): @@ -217,7 +206,7 @@ def start(): start_type = "Dry-" else: start_type = "" - util.separator(f"Starting {start_type}Run") + logger.separator(f"Starting {start_type}Run") cfg = None body = '' run_time = '' @@ -248,20 +237,20 @@ def FinishedRun(): _, nr = calc_next_run(sch, True) next_run_str = nr['next_run_str'] next_run = nr['next_run'] - body = util.separator(f"Finished {start_type}Run\n{os.linesep.join(stats_summary) if len(stats_summary)>0 else ''}\nRun Time: {run_time}\n{next_run_str if len(next_run_str)>0 else ''}" - .replace('\n\n', '\n').rstrip())[0] + body = logger.separator(f"Finished {start_type}Run\n{os.linesep.join(stats_summary) if len(stats_summary)>0 else ''}\nRun Time: {run_time}\n{next_run_str if len(next_run_str)>0 else ''}" + .replace('\n\n', '\n').rstrip())[0] return next_run, body try: cfg = Config(default_dir, args) except Exception as e: if 'Qbittorrent Error' in e.args[0]: - util.print_multiline(e, 'CRITICAL') - util.print_line('Exiting scheduled Run.', 'CRITICAL') + logger.print_line(e, 'CRITICAL') + logger.print_line('Exiting scheduled Run.', 'CRITICAL') FinishedRun() return None else: - util.print_stacktrace() - util.print_multiline(e, 'CRITICAL') + logger.stacktrace() + logger.print_line(e, 'CRITICAL') if cfg: # Set Category @@ -323,18 +312,17 @@ def FinishedRun(): if stats["recycle_emptied"] > 0: stats_summary.append(f"Total Files Deleted from Recycle Bin: {stats['recycle_emptied']}") FinishedRun() - if cfg: try: cfg.Webhooks.end_time_hooks(start_time, end_time, run_time, next_run, stats, body) except Failed as e: - util.print_stacktrace() + logger.stacktrace() logger.error(f"Webhooks Error: {e}") def end(): logger.info("Exiting Qbit_manage") - logger.removeHandler(file_handler) + logger.remove_main_handler() sys.exit(0) @@ -366,18 +354,19 @@ def calc_next_run(sch, print=False): if __name__ == '__main__': killer = GracefulKiller() - util.separator() - logger.info(util.centered(" _ _ _ ")) # noqa: W605 - logger.info(util.centered(" | | (_) | ")) # noqa: W605 - logger.info(util.centered(" __ _| |__ _| |_ _ __ ___ __ _ _ __ __ _ __ _ ___ ")) # noqa: W605 - logger.info(util.centered(" / _` | '_ \| | __| | '_ ` _ \ / _` | '_ \ / _` |/ _` |/ _ \\")) # noqa: W605 - logger.info(util.centered(" | (_| | |_) | | |_ | | | | | | (_| | | | | (_| | (_| | __/")) # noqa: W605 - logger.info(util.centered(" \__, |_.__/|_|\__| |_| |_| |_|\__,_|_| |_|\__,_|\__, |\___|")) # noqa: W605 - logger.info(util.centered(" | | ______ __/ | ")) # noqa: W605 - logger.info(util.centered(" |_| |______| |___/ ")) # noqa: W605 + logger.add_main_handler() + logger.separator() + logger.info_center(" _ _ _ ") # noqa: W605 + logger.info_center(" | | (_) | ") # noqa: W605 + logger.info_center(" __ _| |__ _| |_ _ __ ___ __ _ _ __ __ _ __ _ ___ ") # noqa: W605 + logger.info_center(" / _` | '_ \| | __| | '_ ` _ \ / _` | '_ \ / _` |/ _` |/ _ \\") # noqa: W605 + logger.info_center(" | (_| | |_) | | |_ | | | | | | (_| | | | | (_| | (_| | __/") # noqa: W605 + logger.info_center(" \__, |_.__/|_|\__| |_| |_| |_|\__,_|_| |_|\__,_|\__, |\___|") # noqa: W605 + logger.info_center(" | | ______ __/ | ") # noqa: W605 + logger.info_center(" |_| |______| |___/ ") # noqa: W605 logger.info(f" Version: {version}") - util.separator(loglevel='DEBUG') + logger.separator(loglevel='DEBUG') logger.debug(f" --run (QBT_RUN): {run}") logger.debug(f" --schedule (QBT_SCHEDULE): {sch}") logger.debug(f" --startup-delay (QBT_STARTUP_DELAY): {startupDelay}") @@ -404,7 +393,7 @@ def calc_next_run(sch, print=False): logger.info(" Run Mode: Script will exit after completion.") start_loop() else: - schedule.every(sch).minutes.do(start) + schedule.every(sch).minutes.do(start_loop) time_str, _ = calc_next_run(sch) logger.info(f" Scheduled Mode: Running every {time_str}.") if startupDelay: From 29d51ee78a20a040ccb8e92303575a5c2b557368 Mon Sep 17 00:00:00 2001 From: bobokun Date: Sat, 20 Aug 2022 22:17:51 -0400 Subject: [PATCH 07/11] Adds log secrets for hiding sensitive information --- modules/apprise.py | 1 + modules/bhd.py | 3 ++- modules/notifiarr.py | 3 ++- modules/qbittorrent.py | 4 +++- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/modules/apprise.py b/modules/apprise.py index d1f8d4d7..c20837c7 100644 --- a/modules/apprise.py +++ b/modules/apprise.py @@ -7,6 +7,7 @@ class Apprise: def __init__(self, config, params): self.config = config self.api_url = params["api_url"] + logger.secret(self.api_url) self.notify_url = ",".join(params["notify_url"]) response = self.config.get(self.api_url) if response.status_code != 200: diff --git a/modules/bhd.py b/modules/bhd.py index 561f51b6..db7fcaf3 100644 --- a/modules/bhd.py +++ b/modules/bhd.py @@ -10,6 +10,7 @@ class BeyondHD: def __init__(self, config, params): self.config = config self.apikey = params["apikey"] + logger.secret(self.apikey) json = {"search": "test"} self.search(json) @@ -17,7 +18,7 @@ def search(self, json, path="torrents/"): url = f"{base_url}{path}{self.apikey}" json["action"] = "search" if self.config.trace_mode: - logger.debug(url.replace(self.apikey, "APIKEY")) + logger.debug(url) logger.debug(f"JSON: {json}") try: response = self.config.post(url, json=json) diff --git a/modules/notifiarr.py b/modules/notifiarr.py index 3250b37c..2f400a52 100644 --- a/modules/notifiarr.py +++ b/modules/notifiarr.py @@ -16,6 +16,7 @@ def __init__(self, config, params): self.develop = params["develop"] self.test = params["test"] self.instance = params["instance"] + logger.secret(self.apikey) url, _ = self.get_url("user/validate/") response = self.config.get(url) response_json = None @@ -38,7 +39,7 @@ def __init__(self, config, params): def get_url(self, path): url = f"{dev_url if self.develop else base_url}{'notification/test' if self.test else f'{path}{self.apikey}'}" if self.config.trace_mode: - logger.debug(url.replace(self.apikey, "APIKEY")) + logger.debug(url) if self.test: params = {"event": f"qbitManage-{self.apikey[:5]}", "qbit_client": self.config.data["qbt"]["host"], "instance": self.instance} else: diff --git a/modules/qbittorrent.py b/modules/qbittorrent.py index f002dd9c..a5a7b09c 100644 --- a/modules/qbittorrent.py +++ b/modules/qbittorrent.py @@ -18,7 +18,9 @@ def __init__(self, config, params): self.host = params["host"] self.username = params["username"] self.password = params["password"] - logger.debug(f'Host: {self.host}, Username: {self.username}, Password: {self.password if self.password is None else "[REDACTED]"}') + logger.secret(self.username) + logger.secret(self.password) + logger.debug(f'Host: {self.host}, Username: {self.username}, Password: {self.password}') try: self.client = Client(host=self.host, username=self.username, password=self.password, VERIFY_WEBUI_CERTIFICATE=False) self.client.auth_log_in() From c4681beb4e0902886e5038a5397a5f5d238643dc Mon Sep 17 00:00:00 2001 From: bobokun Date: Sun, 21 Aug 2022 14:39:48 -0400 Subject: [PATCH 08/11] remove unused functions --- modules/util.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/modules/util.py b/modules/util.py index ae10ea69..2978a1d2 100644 --- a/modules/util.py +++ b/modules/util.py @@ -170,23 +170,6 @@ class Failed(Exception): pass -separating_character = "=" -screen_width = 100 -spacing = 0 - - -def tab_new_lines(data): - return str(data).replace("\n", "\n|\t ") if "\n" in str(data) else str(data) - - -def add_dict_list(keys, value, dict_map): - for key in keys: - if key in dict_map: - dict_map[key].append(value) - else: - dict_map[key] = [value] - - def list_in_text(text, search_list, match_all=False): if isinstance(search_list, list): search_list = set(search_list) contains = set([x for x in search_list if ' ' in x]) From 2e15e35ef5f8b7705407d223ed7ea157cdffdd87 Mon Sep 17 00:00:00 2001 From: bobokun Date: Sun, 21 Aug 2022 14:40:57 -0400 Subject: [PATCH 09/11] Adds running commands via config #149 --- config/config.yml.sample | 13 +++++++++++ modules/config.py | 48 +++++++++++++++++++++++++++++++++++----- modules/qbittorrent.py | 36 +++++++++++++++--------------- modules/webhooks.py | 4 ++-- 4 files changed, 75 insertions(+), 26 deletions(-) diff --git a/config/config.yml.sample b/config/config.yml.sample index ebebcaa2..83b246d7 100644 --- a/config/config.yml.sample +++ b/config/config.yml.sample @@ -3,6 +3,19 @@ # Please refer to the link below for more details on how to set up the configuration file # https://github.com/StuffAnThings/qbit_manage/wiki/Config-Setup +commands: + # The commands defined below will IGNORE any commands used in command line and docker env variables. + dry_run: True + cross_seed: False + recheck: False + cat_update: False + tag_update: False + rem_unregistered: False + tag_tracker_error: False + rem_orphaned: False + tag_nohardlinks: False + skip_recycle: False + qbt: # qBittorrent parameters host: "localhost:8080" diff --git a/modules/config.py b/modules/config.py index 8d41788e..3b7bb1d7 100644 --- a/modules/config.py +++ b/modules/config.py @@ -31,6 +31,42 @@ def __init__(self, default_dir, args): loaded_yaml = YAML(self.config_path) self.data = loaded_yaml.data + + # Replace env variables with config commands + if "commands" in self.data: + if self.data["commands"] is not None: + logger.info(f"Commands found in {config_file}, ignoring env variables and using config commands instead.") + self.commands = self.data.pop("commands") + if 'dry_run' not in self.commands: + self.commands['dry_run'] = args['dry_run'] if 'dry_run' in args else False + # Add default any missing commands as False + for v in [ + 'cross_seed', + 'recheck', + 'cat_update', + 'tag_update', + 'rem_unregistered', + 'tag_tracker_error', + 'rem_orphaned', + 'tag_nohardlinks', + 'skip_recycle', + ]: + if v not in self.commands: + self.commands[v] = False + + logger.debug(f" --cross-seed (QBT_CROSS_SEED): {self.commands['cross_seed']}") + logger.debug(f" --recheck (QBT_RECHECK): {self.commands['recheck']}") + logger.debug(f" --cat-update (QBT_CAT_UPDATE): {self.commands['cat_update']}") + logger.debug(f" --tag-update (QBT_TAG_UPDATE): {self.commands['tag_update']}") + logger.debug(f" --rem-unregistered (QBT_REM_UNREGISTERED): {self.commands['rem_unregistered']}") + logger.debug(f" --tag-tracker-error (QBT_TAG_TRACKER_ERROR): {self.commands['tag_tracker_error']}") + logger.debug(f" --rem-orphaned (QBT_REM_ORPHANED): {self.commands['rem_orphaned']}") + logger.debug(f" --tag-nohardlinks (QBT_TAG_NOHARDLINKS): {self.commands['tag_nohardlinks']}") + logger.debug(f" --skip-recycle (QBT_SKIP_RECYCLE): {self.commands['skip_recycle']}") + logger.debug(f" --dry-run (QBT_DRY_RUN): {self.commands['dry_run']}") + else: + self.commands = args + if "qbt" in self.data: self.data["qbt"] = self.data.pop("qbt") self.data["settings"] = self.data.pop("settings") if "settings" in self.data else {} if "directory" in self.data: self.data["directory"] = self.data.pop("directory") @@ -149,7 +185,7 @@ def hooks(attr): # nohardlinks self.nohardlinks = None - if "nohardlinks" in self.data and self.args['tag_nohardlinks']: + if "nohardlinks" in self.data and self.commands['tag_nohardlinks']: self.nohardlinks = {} for cat in self.data["nohardlinks"]: if cat in list(self.data["cat"].keys()): @@ -170,7 +206,7 @@ def hooks(attr): self.notify(e, 'Config') raise Failed(e) else: - if self.args["tag_nohardlinks"]: + if self.commands["tag_nohardlinks"]: e = "Config Error: nohardlinks attribute not found" self.notify(e, 'Config') raise Failed(e) @@ -186,12 +222,12 @@ def hooks(attr): if "directory" in self.data: self.root_dir = os.path.join(self.util.check_for_attribute(self.data, "root_dir", parent="directory", default_is_none=True), '') self.remote_dir = os.path.join(self.util.check_for_attribute(self.data, "remote_dir", parent="directory", default=self.root_dir), '') - if (self.args["cross_seed"] or self.args["tag_nohardlinks"] or self.args["rem_orphaned"]): + if (self.commands["cross_seed"] or self.commands["tag_nohardlinks"] or self.commands["rem_orphaned"]): self.remote_dir = self.util.check_for_attribute(self.data, "remote_dir", parent="directory", var_type="path", default=self.root_dir) else: if self.recyclebin['enabled']: self.remote_dir = self.util.check_for_attribute(self.data, "remote_dir", parent="directory", var_type="path", default=self.root_dir) - if self.args["cross_seed"]: + if self.commands["cross_seed"]: self.cross_seed_dir = self.util.check_for_attribute(self.data, "cross_seed", parent="directory", var_type="path") else: self.cross_seed_dir = self.util.check_for_attribute(self.data, "cross_seed", parent="directory", default_is_none=True) @@ -325,12 +361,12 @@ def get_category(self, path): # Empty the recycle bin def empty_recycle(self): - dry_run = self.args['dry_run'] + dry_run = self.commands['dry_run'] loglevel = 'DRYRUN' if dry_run else 'INFO' num_del = 0 files = [] size_bytes = 0 - if not self.args["skip_recycle"]: + if not self.commands["skip_recycle"]: if self.recyclebin['enabled'] and self.recyclebin['empty_after_x_days']: if self.recyclebin['split_by_category']: if "cat" in self.data and self.data["cat"] is not None: diff --git a/modules/qbittorrent.py b/modules/qbittorrent.py index a5a7b09c..2170b813 100644 --- a/modules/qbittorrent.py +++ b/modules/qbittorrent.py @@ -69,7 +69,7 @@ def __init__(self, config, params): # is_complete = Returns the state of torrent (Returns True if at least one of the torrent with the State is categorized as Complete.) # first_hash = Returns the hash number of the original torrent (Assuming the torrent list is sorted by date added (Asc)) def get_torrent_info(torrent_list): - dry_run = self.config.args['dry_run'] + dry_run = self.config.commands['dry_run'] loglevel = 'DRYRUN' if dry_run else 'INFO' torrentdict = {} t_obj_unreg = [] @@ -152,7 +152,7 @@ def get_torrent_info(torrent_list): self.torrentinfo = None self.torrentissue = None self.torrentvalid = None - if config.args['recheck'] or config.args['cross_seed'] or config.args['rem_unregistered'] or config.args['tag_tracker_error'] or config.args['tag_nohardlinks']: + if config.commands['recheck'] or config.commands['cross_seed'] or config.commands['rem_unregistered'] or config.commands['tag_tracker_error'] or config.commands['tag_nohardlinks']: # Get an updated torrent dictionary information of the torrents self.torrentinfo, self.torrentissue, self.torrentvalid = get_torrent_info(self.torrent_list) @@ -160,7 +160,7 @@ def get_torrents(self, params): return self.client.torrents.info(**params) def category(self): - dry_run = self.config.args['dry_run'] + dry_run = self.config.commands['dry_run'] loglevel = 'DRYRUN' if dry_run else 'INFO' num_cat = 0 @@ -199,7 +199,7 @@ def update_cat(new_cat, cat_change): self.config.send_notifications(attr) num_cat += 1 - if self.config.args['cat_update']: + if self.config.commands['cat_update']: logger.separator("Updating Categories", space=False, border=False) torrent_list = self.get_torrents({'category': '', 'filter': 'completed'}) for torrent in torrent_list: @@ -221,11 +221,11 @@ def update_cat(new_cat, cat_change): return num_cat def tags(self): - dry_run = self.config.args['dry_run'] + dry_run = self.config.commands['dry_run'] loglevel = 'DRYRUN' if dry_run else 'INFO' num_tags = 0 ignore_tags = self.config.settings['ignoreTags_OnUpdate'] - if self.config.args['tag_update']: + if self.config.commands['tag_update']: logger.separator("Updating Tags", space=False, border=False) for torrent in self.torrent_list: check_tags = util.get_list(torrent.tags) @@ -260,7 +260,7 @@ def tags(self): return num_tags def set_tags_and_limits(self, torrent, max_ratio, max_seeding_time, limit_upload_speed=None, tags=None, restore=False): - dry_run = self.config.args['dry_run'] + dry_run = self.config.commands['dry_run'] loglevel = 'DRYRUN' if dry_run else 'INFO' body = [] # Print Logs @@ -296,14 +296,14 @@ def set_tags_and_limits(self, torrent, max_ratio, max_seeding_time, limit_upload return body def tag_nohardlinks(self): - dry_run = self.config.args['dry_run'] + dry_run = self.config.commands['dry_run'] loglevel = 'DRYRUN' if dry_run else 'INFO' num_tags = 0 # counter for the number of torrents that has no hard links del_tor = 0 # counter for the number of torrents that has no hard links and meets the criteria for ratio limit/seed limit for deletion del_tor_cont = 0 # counter for the number of torrents that has no hard links and meets the criteria for ratio limit/seed limit for deletion including contents num_untag = 0 # counter for number of torrents that previously had no hard links but now have hard links - if self.config.args['tag_nohardlinks']: + if self.config.commands['tag_nohardlinks']: logger.separator("Tagging Torrents with No Hardlinks", space=False, border=False) nohardlinks = self.config.nohardlinks tdel_dict = {} # dictionary to track the torrent names and content path that meet the deletion criteria @@ -440,7 +440,7 @@ def tag_nohardlinks(self): return num_tags, num_untag, del_tor, del_tor_cont def rem_unregistered(self): - dry_run = self.config.args['dry_run'] + dry_run = self.config.commands['dry_run'] loglevel = 'DRYRUN' if dry_run else 'INFO' del_tor = 0 del_tor_cont = 0 @@ -448,8 +448,8 @@ def rem_unregistered(self): num_untag = 0 tor_error_summary = '' tag_error = self.config.settings['tracker_error_tag'] - cfg_rem_unregistered = self.config.args['rem_unregistered'] - cfg_tag_error = self.config.args['tag_tracker_error'] + cfg_rem_unregistered = self.config.commands['rem_unregistered'] + cfg_tag_error = self.config.commands['tag_tracker_error'] def tag_tracker_error(): nonlocal dry_run, t_name, msg_up, msg, tracker, t_cat, torrent, tag_error, tor_error_summary, num_tor_error @@ -609,11 +609,11 @@ def del_unregistered(): # Function used to move any torrents from the cross seed directory to the correct save directory def cross_seed(self): - dry_run = self.config.args['dry_run'] + dry_run = self.config.commands['dry_run'] loglevel = 'DRYRUN' if dry_run else 'INFO' added = 0 # Keep track of total torrents tagged tagged = 0 # Track # of torrents tagged that are not cross-seeded - if self.config.args['cross_seed']: + if self.config.commands['cross_seed']: logger.separator("Checking for Cross-Seed Torrents", space=False, border=False) # List of categories for all torrents moved categories = [] @@ -697,11 +697,11 @@ def cross_seed(self): # Function used to recheck paused torrents sorted by size and resume torrents that are completed def recheck(self): - dry_run = self.config.args['dry_run'] + dry_run = self.config.commands['dry_run'] loglevel = 'DRYRUN' if dry_run else 'INFO' resumed = 0 rechecked = 0 - if self.config.args['recheck']: + if self.config.commands['recheck']: logger.separator("Rechecking Paused Torrents", space=False, border=False) # sort by size and paused torrent_list = self.get_torrents({'status_filter': 'paused', 'sort': 'size'}) @@ -764,10 +764,10 @@ def recheck(self): return resumed, rechecked def rem_orphaned(self): - dry_run = self.config.args['dry_run'] + dry_run = self.config.commands['dry_run'] loglevel = 'DRYRUN' if dry_run else 'INFO' orphaned = 0 - if self.config.args['rem_orphaned']: + if self.config.commands['rem_orphaned']: logger.separator("Checking for Orphaned Files", space=False, border=False) torrent_files = [] root_files = [] diff --git a/modules/webhooks.py b/modules/webhooks.py index acdcc954..9dc70b6d 100644 --- a/modules/webhooks.py +++ b/modules/webhooks.py @@ -72,7 +72,7 @@ def _request(self, webhooks, json): def start_time_hooks(self, start_time): if self.run_start_webhooks: - dry_run = self.config.args['dry_run'] + dry_run = self.config.commands['dry_run'] if dry_run: start_type = "Dry-" else: @@ -82,7 +82,7 @@ def start_time_hooks(self, start_time): "title": None, "body": f"Starting {start_type}Run", "start_time": start_time.strftime("%Y-%m-%d %H:%M:%S"), - "dry_run": self.config.args['dry_run'] + "dry_run": self.config.commands['dry_run'] }) def end_time_hooks(self, start_time, end_time, run_time, next_run, stats, body): From 312c65a7e00ec91c84746b8f5390f35799c81f05 Mon Sep 17 00:00:00 2001 From: bobokun Date: Sun, 21 Aug 2022 17:08:13 -0400 Subject: [PATCH 10/11] remove inconsistent dryrun messaging When dryrun is defined in config, it is not reflected correctly in run --- qbit_manage.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/qbit_manage.py b/qbit_manage.py index 89ac7934..11c2b1a2 100644 --- a/qbit_manage.py +++ b/qbit_manage.py @@ -202,11 +202,7 @@ def start(): args["time"] = start_time.strftime("%H:%M") args["time_obj"] = start_time stats_summary = [] - if dry_run: - start_type = "Dry-" - else: - start_type = "" - logger.separator(f"Starting {start_type}Run") + logger.separator("Starting Run") cfg = None body = '' run_time = '' @@ -231,13 +227,13 @@ def start(): } def FinishedRun(): - nonlocal end_time, start_time, start_type, stats_summary, run_time, next_run, body + nonlocal end_time, start_time, stats_summary, run_time, next_run, body end_time = datetime.now() run_time = str(end_time - start_time).split('.')[0] _, nr = calc_next_run(sch, True) next_run_str = nr['next_run_str'] next_run = nr['next_run'] - body = logger.separator(f"Finished {start_type}Run\n{os.linesep.join(stats_summary) if len(stats_summary)>0 else ''}\nRun Time: {run_time}\n{next_run_str if len(next_run_str)>0 else ''}" + body = logger.separator(f"Finished Run\n{os.linesep.join(stats_summary) if len(stats_summary)>0 else ''}\nRun Time: {run_time}\n{next_run_str if len(next_run_str)>0 else ''}" .replace('\n\n', '\n').rstrip())[0] return next_run, body try: From 6215316e0f8daa1f6072d40f9e70271642cb4dd5 Mon Sep 17 00:00:00 2001 From: Jon Lee Date: Fri, 26 Aug 2022 08:11:54 -0400 Subject: [PATCH 11/11] update requirements --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index adf44697..35d671b8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ ruamel.yaml==0.17.21 -qbittorrent-api>=2022.8.35 +qbittorrent-api>=2022.8.37 schedule==1.1.0 retrying==1.3.3 alive_progress==2.4.1