apprise integration

This commit is contained in:
bobokun 2021-12-18 12:38:41 -05:00
parent beb34c4f72
commit a7b20bf6a4
No known key found for this signature in database
GPG key ID: 9665BA6CF5DC2671
9 changed files with 166 additions and 84 deletions

View file

@ -18,6 +18,7 @@ This is a program used to manage your qBittorrent instance such as:
* Tag any torrents that have no hard links and allows optional cleanup to delete these torrents and contents based on maximum ratio and/or time seeded
* RecycleBin function to move files into a RecycleBin folder instead of deleting the data directly when deleting a torrent
* Built-in scheduler to run the script every x minutes. (Can use `--run` command to run without the scheduler)
* Webhook notifications with [Notifiarr](https://notifiarr.com/) and [Apprise API](https://github.com/caronc/apprise-api) integration
## Getting Started
Check out the [wiki](https://github.com/StuffAnThings/qbit_manage/wiki) for installation help

View file

@ -128,16 +128,25 @@ orphaned:
- "**/@eaDir"
- "/data/torrents/temp/**"
#Apprise integration with webhooks
apprise:
#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
notifiarr:
#Mandatory to fill out API Key
apikey: ####################################
#Your qBittorrent instance, can be set to any unique value
#<OPTIONAL> Set to a unique value (could be your username on notifiarr for example)
instance:
test: true
develop: true
# Webhook notifications: Set value to notifiarr if using notifiarr integration, otherwise set to webhook URL
# 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
webhooks:
error: notifiarr
run_start: notifiarr

14
modules/apprise.py Normal file
View file

@ -0,0 +1,14 @@
import logging
from modules.util import Failed
logger = logging.getLogger("qBit Manage")
class Apprise:
def __init__(self, config, params):
self.config = config
self.api_url = params["api_url"]
self.notify_url = ",".join(params["notify_url"])
response = self.config.get(self.api_url)
if response.status_code != 200:
raise Failed(f"Apprise Error: Unable to connect to Apprise using {self.api_url}")

View file

@ -4,6 +4,7 @@ from modules.util import Failed, check
from modules.qbittorrent import Qbt
from modules.webhooks import Webhooks
from modules.notifiarr import Notifiarr
from modules.apprise import Apprise
from ruamel import yaml
from retrying import retry
@ -38,6 +39,7 @@ class Config:
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")
@ -75,6 +77,22 @@ class Config:
"function": self.util.check_for_attribute(self.data, "function", parent="webhooks", var_type="list", default_is_none=True)
}
self.AppriseFactory = None
if "apprise" in self.data:
if self.data["apprise"] is not None:
logger.info("Connecting to Apprise...")
try:
self.AppriseFactory = Apprise(self, {
"api_url": self.util.check_for_attribute(self.data, "api_url", parent="apprise", var_type="url", throw=True),
"notify_url": self.util.check_for_attribute(self.data, "notify_url", parent="apprise", var_type="list", throw=True),
})
except Failed as e:
logger.error(e)
logger.info(f"Apprise Connection {'Failed' if self.AppriseFactory is None else 'Successful'}")
else:
logger.warning("Config Warning: apprise attribute not found")
self.NotifiarrFactory = None
if "notifiarr" in self.data:
if self.data["notifiarr"] is not None:
@ -92,7 +110,7 @@ class Config:
else:
logger.warning("Config Warning: notifiarr attribute not found")
self.Webhooks = Webhooks(self, self.webhooks, notifiarr=self.NotifiarrFactory)
self.Webhooks = Webhooks(self, self.webhooks, notifiarr=self.NotifiarrFactory,apprise=self.AppriseFactory)
try:
self.Webhooks.start_time_hooks(self.start_time)
except Failed as e:
@ -267,16 +285,18 @@ class Config:
if not dry_run: os.remove(file)
if num_del > 0:
if not dry_run: util.remove_empty_directories(self.recycle_dir,"**/*")
body = []
body += util.print_multiline(n_info,loglevel)
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)
attr = {
"function":"empty_recyclebin",
"title":f"Emptying Recycle Bin (Files > {self.recyclebin['empty_after_x_days']} days)",
"body": "\n".join(body),
"files":files,
"empty_after_x_days": self.recyclebin['empty_after_x_days'],
"size_in_bytes":size_bytes
}
self.send_notifications(attr)
util.print_multiline(n_info,loglevel)
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)
else:
logger.debug('No files found in "' + self.recycle_dir + '"')
return num_del

View file

@ -28,6 +28,8 @@ class Notifiarr:
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"))
test_payload = (f"qbitManage-{self.apikey[:5]}")
params = {"event": test_payload, "qbit_client":self.config.data["qbt"]["host"], "instance":self.instance if self.test else "notify"}
if self.test:
params = {"event": f"qbitManage-{self.apikey[:5]}", "qbit_client":self.config.data["qbt"]["host"], "instance":self.instance}
else:
params = {"qbit_client":self.config.data["qbt"]["host"], "instance":self.instance}
return url, params

View file

@ -104,12 +104,14 @@ class Qbt:
new_cat = self.config.get_category(torrent.save_path)
tags = self.config.get_tags([x.url for x in torrent.trackers if x.url.startswith('http')])
if not dry_run: torrent.set_category(category=new_cat)
print_line(util.insert_space(f'Torrent Name: {torrent.name}',3),loglevel)
print_line(util.insert_space(f'New Category: {new_cat}',3),loglevel)
print_line(util.insert_space(f'Tracker: {tags["url"]}',8),loglevel)
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: {tags["url"]}',8),loglevel)
attr = {
"function":"cat_update",
"title":"Updating Categories",
"body": "\n".join(body),
"torrent_name":torrent.name,
"torrent_new_cat": new_cat,
"torrent_tracker": tags["url"],
@ -134,13 +136,15 @@ class Qbt:
tags = self.config.get_tags([x.url for x in torrent.trackers if x.url.startswith('http')])
if tags["new_tag"]:
num_tags += 1
print_line(util.insert_space(f'Torrent Name: {torrent.name}',3),loglevel)
print_line(util.insert_space(f'New Tag: {tags["new_tag"]}',8),loglevel)
print_line(util.insert_space(f'Tracker: {tags["url"]}',8),loglevel)
self.set_tags_and_limits(torrent, tags["max_ratio"], tags["max_seeding_time"],tags["limit_upload_speed"],tags["new_tag"])
body = []
body += print_line(util.insert_space(f'Torrent Name: {torrent.name}',3),loglevel)
body += print_line(util.insert_space(f'New Tag: {tags["new_tag"]}',8),loglevel)
body += print_line(util.insert_space(f'Tracker: {tags["url"]}',8),loglevel)
body.extend(self.set_tags_and_limits(torrent, tags["max_ratio"], tags["max_seeding_time"],tags["limit_upload_speed"],tags["new_tag"]))
attr = {
"function":"tag_update",
"title":"Updating Tags",
"body": "\n".join(body),
"torrent_name":torrent.name,
"torrent_new_tag": tags["new_tag"],
"torrent_tracker": tags["url"],
@ -158,21 +162,22 @@ class Qbt:
def set_tags_and_limits(self,torrent,max_ratio,max_seeding_time,limit_upload_speed=None,tags=None):
dry_run = self.config.args['dry_run']
loglevel = 'DRYRUN' if dry_run else 'INFO'
loglevel = 'DRYRUN' if dry_run else 'INFO'
body = []
#Print Logs
if limit_upload_speed:
if limit_upload_speed == -1: print_line(util.insert_space(f'Limit UL Speed: Infinity',1),loglevel)
else: print_line(util.insert_space(f'Limit UL Speed: {limit_upload_speed} kB/s',1),loglevel)
if limit_upload_speed == -1: body += print_line(util.insert_space(f'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 max_ratio or max_seeding_time:
if max_ratio == -2 or max_seeding_time == -2: print_line(util.insert_space(f'Share Limit: Use Global Share Limit',4),loglevel)
elif max_ratio == -1 or max_seeding_time == -1: print_line(util.insert_space(f'Share Limit: Set No Share Limit',4),loglevel)
if max_ratio == -2 or max_seeding_time == -2: body += print_line(util.insert_space(f'Share Limit: Use Global Share Limit',4),loglevel)
elif max_ratio == -1 or max_seeding_time == -1: body += print_line(util.insert_space(f'Share Limit: Set No Share Limit',4),loglevel)
else:
if max_ratio != torrent.max_ratio and not max_seeding_time:
print_line(util.insert_space(f'Share Limit: Max Ratio = {max_ratio}',4),loglevel)
body += print_line(util.insert_space(f'Share Limit: Max Ratio = {max_ratio}',4),loglevel)
elif max_seeding_time != torrent.max_seeding_time and not max_ratio:
print_line(util.insert_space(f'Share Limit: Max Seed Time = {max_seeding_time} min',4),loglevel)
body += print_line(util.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:
print_line(util.insert_space(f'Share Limit: Max Ratio = {max_ratio}, Max Seed Time = {max_seeding_time} min',4),loglevel)
body += print_line(util.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)
@ -189,6 +194,7 @@ class Qbt:
if not max_ratio: max_ratio = torrent.max_ratio
if not max_seeding_time: max_seeding_time = torrent.max_seeding_time
torrent.set_share_limits(max_ratio,max_seeding_time)
return body
def tag_nohardlinks(self):
dry_run = self.config.args['dry_run']
@ -222,13 +228,15 @@ class Qbt:
#Will only tag new torrents that don't have noHL tag
if 'noHL' not in torrent.tags :
num_tags += 1
print_line(util.insert_space(f'Torrent Name: {torrent.name}',3),loglevel)
print_line(util.insert_space(f'Added Tag: noHL',6),loglevel)
print_line(util.insert_space(f'Tracker: {tags["url"]}',8),loglevel)
self.set_tags_and_limits(torrent, nohardlinks[category]["max_ratio"], nohardlinks[category]["max_seeding_time"],nohardlinks[category]["limit_upload_speed"],tags='noHL')
body = []
body += print_line(util.insert_space(f'Torrent Name: {torrent.name}',3),loglevel)
body += print_line(util.insert_space(f'Added Tag: noHL',6),loglevel)
body += print_line(util.insert_space(f'Tracker: {tags["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 = {
"function":"tag_nohardlinks",
"title":"Tagging Torrents with No Hardlinks",
"body": "\n".join(body),
"torrent_name":torrent.name,
"torrent_add_tag": 'noHL',
"torrent_tracker": tags["url"],
@ -246,13 +254,18 @@ class Qbt:
#Checks to see if previous noHL tagged torrents now have hard links.
if (not (util.nohardlink(torrent['content_path'].replace(root_dir,root_dir))) and ('noHL' in torrent.tags)):
num_untag += 1
print_line(f'Previous Tagged noHL Torrent Name: {torrent.name} has hard links found now.',loglevel)
print_line(util.insert_space(f'Removed Tag: noHL',6),loglevel)
print_line(util.insert_space(f'Tracker: {tags["url"]}',8),loglevel)
print_line(f"{'Not Reverting' if dry_run else 'Reverting'} share limits.",loglevel)
body = []
body += print_line(f'Previous Tagged noHL Torrent Name: {torrent.name} has hard links found now.',loglevel)
body += print_line(util.insert_space(f'Removed Tag: noHL',6),loglevel)
body += print_line(util.insert_space(f'Tracker: {tags["url"]}',8),loglevel)
body += print_line(f"{'Not Reverting' if dry_run else 'Reverting'} share limits.",loglevel)
if not dry_run:
torrent.remove_tags(tags='noHL')
body.extend(self.set_tags_and_limits(torrent, tags["max_ratio"], tags["max_seeding_time"],tags["limit_upload_speed"]))
attr = {
"function":"untag_nohardlinks",
"title":"Untagging Previous Torrents that now have Hard Links",
"body": "\n".join(body),
"torrent_name":torrent.name,
"torrent_remove_tag": 'noHL',
"torrent_tracker": tags["url"],
@ -262,9 +275,6 @@ class Qbt:
"torrent_limit_upload_speed": tags["limit_upload_speed"]
}
self.config.send_notifications(attr)
if not dry_run:
torrent.remove_tags(tags='noHL')
self.set_tags_and_limits(torrent, tags["max_ratio"], tags["max_seeding_time"],tags["limit_upload_speed"])
#loop through torrent list again for cleanup purposes
if (nohardlinks[category]['cleanup']):
for torrent in torrent_list:
@ -272,27 +282,29 @@ class Qbt:
#Double check that the content path is the same before we delete anything
if torrent['content_path'].replace(root_dir,root_dir) == tdel_dict[torrent.name]:
tags = self.config.get_tags([x.url for x in torrent.trackers if x.url.startswith('http')])
print_line(util.insert_space(f'Torrent Name: {torrent.name}',3),loglevel)
print_line(util.insert_space(f'Tracker: {tags["url"]}',8),loglevel)
print_line(util.insert_space(f"Cleanup: True [No hard links found and meets Share Limits.]",8),loglevel)
body = []
body += print_line(util.insert_space(f'Torrent Name: {torrent.name}',3),loglevel)
body += print_line(util.insert_space(f'Tracker: {tags["url"]}',8),loglevel)
body += print_line(util.insert_space(f"Cleanup: True [No hard links found and meets Share Limits.]",8),loglevel)
if (os.path.exists(torrent['content_path'].replace(root_dir,root_dir))):
if not dry_run: self.tor_delete_recycle(torrent)
del_tor_cont += 1
attr["torrents_deleted_and_contents"]: True
body += print_line(util.insert_space(f'Deleted .torrent AND content files.',8),loglevel)
else:
if not dry_run: torrent.delete(hash=torrent.hash, delete_files=False)
del_tor += 1
attr["torrents_deleted_and_contents"]: False
body += print_line(util.insert_space(f'Deleted .torrent but NOT content files.',8),loglevel)
attr = {
"function":"cleanup_tag_nohardlinks",
"title":"Removing NoHL Torrents and meets Share Limits",
"body": "\n".join(body),
"torrent_name":torrent.name,
"cleanup": 'True',
"torrent_tracker": tags["url"],
"notifiarr_indexer": tags["notifiarr"],
}
if (os.path.exists(torrent['content_path'].replace(root_dir,root_dir))):
if not dry_run: self.tor_delete_recycle(torrent)
del_tor_cont += 1
attr["torrents_deleted_and_contents"]: True
print_line(util.insert_space(f'Deleted .torrent AND content files.',8),loglevel)
else:
if not dry_run: torrent.delete(hash=torrent.hash, delete_files=False)
del_tor += 1
attr["torrents_deleted_and_contents"]: False
print_line(util.insert_space(f'Deleted .torrent but NOT content files.',8),loglevel)
self.config.send_notifications(attr)
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)
@ -345,6 +357,7 @@ class Qbt:
attr = {
"function":"potential_rem_unregistered",
"title":"Potential Unregistered Torrents",
"body": pot_unr,
"torrent_name":t_name,
"torrent_status": msg_up,
"torrent_tracker": tags["url"],
@ -352,34 +365,36 @@ class Qbt:
}
self.config.send_notifications(attr)
if any(m in msg_up for m in unreg_msgs) and x.status == 4 and 'DOWN' not in msg_up and 'UNREACHABLE' not in msg_up:
print_line(util.insert_space(f'Torrent Name: {t_name}',3),loglevel)
print_line(util.insert_space(f'Status: {msg_up}',9),loglevel)
print_line(util.insert_space(f'Tracker: {tags["url"]}',8),loglevel)
attr = {
"function":"rem_unregistered",
"title":"Removing Unregistered Torrents",
"torrent_name":t_name,
"torrent_status": msg_up,
"torrent_tracker": tags["url"],
"notifiarr_indexer": tags["notifiarr"],
}
body = []
body += print_line(util.insert_space(f'Torrent Name: {t_name}',3),loglevel)
body += print_line(util.insert_space(f'Status: {msg_up}',9),loglevel)
body += print_line(util.insert_space(f'Tracker: {tags["url"]}',8),loglevel)
if t_count > 1:
# Checks if any of the original torrents are working
if '' in t_msg or 2 in t_status:
if not dry_run: torrent.delete(hash=torrent.hash, delete_files=False)
attr["torrents_deleted_and_contents"]: False
print_line(util.insert_space(f'Deleted .torrent but NOT content files.',8),loglevel)
body += print_line(util.insert_space(f'Deleted .torrent but NOT content files.',8),loglevel)
del_tor += 1
else:
if not dry_run: self.tor_delete_recycle(torrent)
attr["torrents_deleted_and_contents"]: True
print_line(util.insert_space(f'Deleted .torrent AND content files.',8),loglevel)
body += print_line(util.insert_space(f'Deleted .torrent AND content files.',8),loglevel)
del_tor_cont += 1
else:
if not dry_run: self.tor_delete_recycle(torrent)
attr["torrents_deleted_and_contents"]: True
print_line(util.insert_space(f'Deleted .torrent AND content files.',8),loglevel)
body += print_line(util.insert_space(f'Deleted .torrent AND content files.',8),loglevel)
del_tor_cont += 1
attr = {
"function":"rem_unregistered",
"title":"Removing Unregistered Torrents",
"body": "\n".join(body),
"torrent_name":t_name,
"torrent_status": msg_up,
"torrent_tracker": tags["url"],
"notifiarr_indexer": tags["notifiarr"],
}
self.config.send_notifications(attr)
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)
@ -423,13 +438,15 @@ class Qbt:
#Only add cross-seed torrent if original torrent is complete
if self.torrentinfo[t_name]['is_complete']:
categories.append(category)
print_line(f"{'Not Adding' if dry_run else 'Adding'} to qBittorrent:",loglevel)
print_line(util.insert_space(f'Torrent Name: {t_name}',3),loglevel)
print_line(util.insert_space(f'Category: {category}',7),loglevel)
print_line(util.insert_space(f'Save_Path: {dest}',6),loglevel)
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)
attr = {
"function":"cross_seed",
"title":"Adding New Cross-Seed Torrent",
"body": "\n".join(body),
"torrent_name":t_name,
"torrent_category": category,
"torrent_save_path": dest,
@ -451,14 +468,15 @@ class Qbt:
t_name = torrent.name
if 'cross-seed' not in torrent.tags and self.torrentinfo[t_name]['count'] > 1 and self.torrentinfo[t_name]['first_hash'] != torrent.hash:
tagged += 1
body = 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",
"body":body,
"torrent_name":t_name,
"torrent_add_tag": "cross-seed"
}
self.config.send_notifications(attr)
print_line(f"{'Not Adding' if dry_run else 'Adding'} 'cross-seed' tag to {t_name}",loglevel)
if not dry_run: torrent.add_tags(tags='cross-seed')
numcategory = Counter(categories)
@ -485,10 +503,11 @@ class Qbt:
if torrent.progress == 1:
if torrent.max_ratio < 0 and torrent.max_seeding_time < 0:
resumed += 1
print_line(f"{'Not Resuming' if dry_run else 'Resuming'} [{tags['new_tag']}] - {torrent.name}",loglevel)
body = print_line(f"{'Not Resuming' if dry_run else 'Resuming'} [{tags['new_tag']}] - {torrent.name}",loglevel)
attr = {
"function":"recheck",
"title":"Resuming Torrent",
"body": body,
"torrent_name":torrent.name,
"torrent_tracker": tags["url"],
"notifiarr_indexer": tags["notifiarr"],
@ -505,10 +524,11 @@ class Qbt:
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
print_line(f"{'Not Resuming' if dry_run else 'Resuming'} [{tags['new_tag']}] - {torrent.name}",loglevel)
body = print_line(f"{'Not Resuming' if dry_run else 'Resuming'} [{tags['new_tag']}] - {torrent.name}",loglevel)
attr = {
"function":"recheck",
"title":"Resuming Torrent",
"body": body,
"torrent_name":torrent.name,
"torrent_tracker": tags["url"],
"notifiarr_indexer": tags["notifiarr"],
@ -518,15 +538,16 @@ class Qbt:
#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'} [{tags['new_tag']}] - {torrent.name}",loglevel)
attr = {
"function":"recheck",
"title":"Rechecking Torrent",
"body": body,
"torrent_name":torrent.name,
"torrent_tracker": tags["url"],
"notifiarr_indexer": tags["notifiarr"],
}
self.config.send_notifications(attr)
print_line(f"{'Not Rechecking' if dry_run else 'Rechecking'} [{tags['new_tag']}] - {torrent.name}",loglevel)
if not dry_run: torrent.recheck()
return resumed,rechecked
@ -576,16 +597,19 @@ class Qbt:
if orphaned_files:
dir_out = os.path.join(remote_path,'orphaned_data')
os.makedirs(dir_out,exist_ok=True)
print_line(f"{len(orphaned_files)} Orphaned files found",loglevel)
print_multiline("\n".join(orphaned_files),loglevel)
print_line(f"{'Did not move' if dry_run else 'Moved'} {len(orphaned_files)} Orphaned files to {dir_out.replace(remote_path,root_path)}",loglevel)
orphaned = len(orphaned_files)
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)
attr = {
"function":"rem_orphaned",
"title":f"Removing {len(orphaned_files)} Orphaned Files",
"title":f"Removing {num_orphaned} Orphaned Files",
"body": "\n".join(body),
"orphaned_files": list(orphaned_files),
"orphaned_directory": dir_out.replace(remote_path,root_path),
"total_orphaned_files": orphaned,
"total_orphaned_files": num_orphaned,
}
self.config.send_notifications(attr)
#Delete empty directories after moving orphan files

View file

@ -177,6 +177,7 @@ def get_int_list(data, id_type):
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")):
@ -184,6 +185,7 @@ def print_multiline(lines, loglevel='INFO'):
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')
@ -220,6 +222,7 @@ def separator(text=None, space=True, border=True, loglevel='INFO'):
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"

View file

@ -6,7 +6,7 @@ from modules.util import Failed
logger = logging.getLogger("qBit Manage")
class Webhooks:
def __init__(self, config, system_webhooks, notifiarr=None):
def __init__(self, config, system_webhooks, notifiarr=None, apprise=None):
self.config = config
self.error_webhooks = system_webhooks["error"] if "error" in system_webhooks else []
self.run_start_webhooks = system_webhooks["run_start"] if "run_start" in system_webhooks else []
@ -19,6 +19,7 @@ class Webhooks:
else:
self.function_webhooks = []
self.notifiarr = notifiarr
self.apprise = apprise
def _request(self, webhooks, json):
if self.config.trace_mode:
@ -28,11 +29,16 @@ class Webhooks:
if self.config.trace_mode:
logger.debug(f"Webhook: {webhook}")
if webhook == "notifiarr":
url, params = self.notifiarr.get_url("notification/qbtManage/")
url, params = self.notifiarr.get_url("notification/qbitManage/")
for x in range(6):
response = self.config.get(url, json=json, params=params)
if response.status_code < 500:
break
elif webhook == "apprise":
if self.apprise is None:
raise Failed(f"Webhook attribute set to apprise but apprise attribute is not configured.")
json['urls'] = self.apprise.notify_url
response = self.config.post(f"{self.apprise.api_url}/notify", json=json)
else:
response = self.config.post(webhook, json=json)
try:
@ -56,12 +62,13 @@ class Webhooks:
start_type = ""
self._request(self.run_start_webhooks, {
"function":"run_start",
"title":f"Starting {start_type}Run",
"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']
})
def end_time_hooks(self, start_time, end_time, run_time, stats):
def end_time_hooks(self, start_time, end_time, run_time, stats, body):
dry_run = self.config.args['dry_run']
if dry_run:
start_type = "Dry-"
@ -70,7 +77,8 @@ class Webhooks:
if self.run_end_webhooks:
self._request(self.run_end_webhooks, {
"function":"run_end",
"title":f"Finished {start_type}Run",
"title": None,
"body": body,
"start_time": start_time.strftime("%Y-%m-%d %H:%M:%S"),
"end_time": end_time.strftime("%Y-%m-%d %H:%M:%S"),
"run_time": run_time,
@ -90,7 +98,8 @@ class Webhooks:
def error_hooks(self, text, function_error=None, critical=True):
if self.error_webhooks:
json = {"function":"run_error","title":f"{function_error} Error","error": str(text), "critical": critical}
type = "failure" if critical == True else "warning"
json = {"function":"run_error","title":f"{function_error} Error","body": str(text), "critical": critical, "type": type}
if function_error:
json["function_error"] = function_error
self._request(self.error_webhooks, json)

View file

@ -227,13 +227,13 @@ def start():
end_time = datetime.now()
run_time = str(end_time - start_time).split('.')[0]
body = util.separator(f"Finished {start_type}Run\n{os.linesep.join(stats_summary) if len(stats_summary)>0 else ''}\nRun Time: {run_time}".replace('\n\n', '\n'))[0]
if cfg:
try:
cfg.Webhooks.end_time_hooks(start_time, end_time, run_time, stats)
cfg.Webhooks.end_time_hooks(start_time, end_time, run_time, stats, body)
except Failed as e:
util.print_stacktrace()
logger.error(f"Webhooks Error: {e}")
util.separator(f"Finished {start_type}Run\n{os.linesep.join(stats_summary) if len(stats_summary)>0 else ''}\nRun Time: {run_time}".replace('\n\n', '\n'))
def end():
logger.info("Exiting Qbit_manage")
logger.removeHandler(file_handler)