mirror of
https://github.com/StuffAnThings/qbit_manage.git
synced 2025-11-08 07:20:59 +08:00
commit
30c3beae2d
17 changed files with 1493 additions and 1298 deletions
12
CHANGELOG
12
CHANGELOG
|
|
@ -1,6 +1,8 @@
|
|||
# Bug Fixes
|
||||
- Fixes #255
|
||||
- Fixes #260
|
||||
- Fixes #258
|
||||
# Requirements Updated
|
||||
- Updates qbitorrent api to 2023.4.45
|
||||
- Updates Schedule to 1.2.0
|
||||
|
||||
**Full Changelog**: https://github.com/StuffAnThings/qbit_manage/compare/v3.5.0...v3.5.1
|
||||
# Refactoring
|
||||
- Refactor qbit_manage to split up core functions into separate files
|
||||
|
||||
**Full Changelog**: https://github.com/StuffAnThings/qbit_manage/compare/v3.5.1...v3.6.0
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
[](https://github.com/StuffAnThings/qbit_manage/releases)
|
||||
[](https://github.com/StuffAnThings/qbit_manage/tree/develop)
|
||||
[](https://hub.docker.com/r/bobokun/qbit_manage)
|
||||

|
||||

|
||||
[](https://results.pre-commit.ci/latest/github/StuffAnThings/qbit_manage/master)
|
||||
[](https://hub.docker.com/r/bobokun/qbit_manage)
|
||||
[](https://github.com/sponsors/bobokun)
|
||||
|
|
|
|||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
3.5.1
|
||||
3.6.0
|
||||
|
|
|
|||
20
modules/__init__.py
Normal file
20
modules/__init__.py
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import os
|
||||
|
||||
# Define an empty version_info tuple
|
||||
__version_info__ = ()
|
||||
|
||||
# Get the path to the project directory
|
||||
project_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
# Get the path to the VERSION file
|
||||
version_file_path = os.path.join(project_dir, "..", "VERSION")
|
||||
|
||||
# Read the version from the file
|
||||
with open(version_file_path) as f:
|
||||
version_str = f.read().strip()
|
||||
|
||||
# Convert the version string to a tuple of integers
|
||||
__version_info__ = tuple(map(int, version_str.split(".")))
|
||||
|
||||
# Define the version string using the version_info tuple
|
||||
__version__ = ".".join(str(i) for i in __version_info__)
|
||||
|
|
@ -474,240 +474,6 @@ class Config:
|
|||
self.notify(e, "Config")
|
||||
raise Failed(e)
|
||||
|
||||
# Get tags from config file based on keyword
|
||||
def get_tags(self, trackers):
|
||||
urls = [x.url for x in trackers if x.url.startswith("http")]
|
||||
tracker = {}
|
||||
tracker["tag"] = None
|
||||
tracker["max_ratio"] = None
|
||||
tracker["min_seeding_time"] = None
|
||||
tracker["max_seeding_time"] = None
|
||||
tracker["limit_upload_speed"] = None
|
||||
tracker["notifiarr"] = None
|
||||
tracker["url"] = None
|
||||
tracker_other_tag = self.util.check_for_attribute(
|
||||
self.data, "tag", parent="tracker", subparent="other", default_is_none=True, var_type="list", save=False
|
||||
)
|
||||
try:
|
||||
tracker["url"] = util.trunc_val(urls[0], os.sep)
|
||||
except IndexError as e:
|
||||
tracker["url"] = None
|
||||
if not urls:
|
||||
urls = []
|
||||
if not tracker_other_tag:
|
||||
tracker_other_tag = ["other"]
|
||||
tracker["url"] = "No http URL found"
|
||||
else:
|
||||
logger.debug(f"Tracker Url:{urls}")
|
||||
logger.debug(e)
|
||||
if "tracker" in self.data and self.data["tracker"] is not None:
|
||||
tag_values = self.data["tracker"]
|
||||
for tag_url, tag_details in tag_values.items():
|
||||
for url in urls:
|
||||
if tag_url in url:
|
||||
if tracker["url"] is None:
|
||||
default_tag = tracker_other_tag
|
||||
else:
|
||||
try:
|
||||
tracker["url"] = util.trunc_val(url, os.sep)
|
||||
default_tag = tracker["url"].split(os.sep)[2].split(":")[0]
|
||||
except IndexError as e:
|
||||
logger.debug(f"Tracker Url:{url}")
|
||||
logger.debug(e)
|
||||
# Tracker Format 1 deprecated.
|
||||
if isinstance(tag_details, str):
|
||||
e = (
|
||||
"Config Error: Tracker format invalid. Please see config.yml.sample for correct format and fix "
|
||||
f"`{tag_details}` in the Tracker section of the config."
|
||||
)
|
||||
self.notify(e, "Config")
|
||||
raise Failed(e)
|
||||
# Using new Format
|
||||
else:
|
||||
tracker["tag"] = self.util.check_for_attribute(
|
||||
self.data, "tag", parent="tracker", subparent=tag_url, default=tag_url, var_type="list"
|
||||
)
|
||||
if tracker["tag"] == [tag_url]:
|
||||
self.data["tracker"][tag_url]["tag"] = [tag_url]
|
||||
if isinstance(tracker["tag"], str):
|
||||
tracker["tag"] = [tracker["tag"]]
|
||||
is_max_ratio_defined = self.data["tracker"].get("max_ratio")
|
||||
is_max_seeding_time_defined = self.data["tracker"].get("max_seeding_time")
|
||||
if is_max_ratio_defined or is_max_seeding_time_defined:
|
||||
tracker["max_ratio"] = self.util.check_for_attribute(
|
||||
self.data,
|
||||
"max_ratio",
|
||||
parent="tracker",
|
||||
subparent=tag_url,
|
||||
var_type="float",
|
||||
min_int=-2,
|
||||
do_print=False,
|
||||
default=-1,
|
||||
save=False,
|
||||
)
|
||||
tracker["max_seeding_time"] = self.util.check_for_attribute(
|
||||
self.data,
|
||||
"max_seeding_time",
|
||||
parent="tracker",
|
||||
subparent=tag_url,
|
||||
var_type="int",
|
||||
min_int=-2,
|
||||
do_print=False,
|
||||
default=-1,
|
||||
save=False,
|
||||
)
|
||||
else:
|
||||
tracker["max_ratio"] = self.util.check_for_attribute(
|
||||
self.data,
|
||||
"max_ratio",
|
||||
parent="tracker",
|
||||
subparent=tag_url,
|
||||
var_type="float",
|
||||
min_int=-2,
|
||||
do_print=False,
|
||||
default_is_none=True,
|
||||
save=False,
|
||||
)
|
||||
tracker["max_seeding_time"] = self.util.check_for_attribute(
|
||||
self.data,
|
||||
"max_seeding_time",
|
||||
parent="tracker",
|
||||
subparent=tag_url,
|
||||
var_type="int",
|
||||
min_int=-2,
|
||||
do_print=False,
|
||||
default_is_none=True,
|
||||
save=False,
|
||||
)
|
||||
tracker["min_seeding_time"] = self.util.check_for_attribute(
|
||||
self.data,
|
||||
"min_seeding_time",
|
||||
parent="tracker",
|
||||
subparent=tag_url,
|
||||
var_type="int",
|
||||
min_int=0,
|
||||
do_print=False,
|
||||
default=0,
|
||||
save=False,
|
||||
)
|
||||
tracker["limit_upload_speed"] = self.util.check_for_attribute(
|
||||
self.data,
|
||||
"limit_upload_speed",
|
||||
parent="tracker",
|
||||
subparent=tag_url,
|
||||
var_type="int",
|
||||
min_int=-1,
|
||||
do_print=False,
|
||||
default=0,
|
||||
save=False,
|
||||
)
|
||||
tracker["notifiarr"] = self.util.check_for_attribute(
|
||||
self.data,
|
||||
"notifiarr",
|
||||
parent="tracker",
|
||||
subparent=tag_url,
|
||||
default_is_none=True,
|
||||
do_print=False,
|
||||
save=False,
|
||||
)
|
||||
return tracker
|
||||
if tracker_other_tag:
|
||||
tracker["tag"] = tracker_other_tag
|
||||
tracker["max_ratio"] = self.util.check_for_attribute(
|
||||
self.data,
|
||||
"max_ratio",
|
||||
parent="tracker",
|
||||
subparent="other",
|
||||
var_type="float",
|
||||
min_int=-2,
|
||||
do_print=False,
|
||||
default=-1,
|
||||
save=False,
|
||||
)
|
||||
tracker["min_seeding_time"] = self.util.check_for_attribute(
|
||||
self.data,
|
||||
"min_seeding_time",
|
||||
parent="tracker",
|
||||
subparent="other",
|
||||
var_type="int",
|
||||
min_int=0,
|
||||
do_print=False,
|
||||
default=-1,
|
||||
save=False,
|
||||
)
|
||||
tracker["max_seeding_time"] = self.util.check_for_attribute(
|
||||
self.data,
|
||||
"max_seeding_time",
|
||||
parent="tracker",
|
||||
subparent="other",
|
||||
var_type="int",
|
||||
min_int=-2,
|
||||
do_print=False,
|
||||
default=-1,
|
||||
save=False,
|
||||
)
|
||||
tracker["limit_upload_speed"] = self.util.check_for_attribute(
|
||||
self.data,
|
||||
"limit_upload_speed",
|
||||
parent="tracker",
|
||||
subparent="other",
|
||||
var_type="int",
|
||||
min_int=-1,
|
||||
do_print=False,
|
||||
default=0,
|
||||
save=False,
|
||||
)
|
||||
tracker["notifiarr"] = self.util.check_for_attribute(
|
||||
self.data,
|
||||
"notifiarr",
|
||||
parent="tracker",
|
||||
subparent="other",
|
||||
default_is_none=True,
|
||||
do_print=False,
|
||||
save=False,
|
||||
)
|
||||
return tracker
|
||||
if tracker["url"]:
|
||||
logger.trace(f"tracker url: {tracker['url']}")
|
||||
if tracker_other_tag:
|
||||
default_tag = tracker_other_tag
|
||||
else:
|
||||
default_tag = tracker["url"].split(os.sep)[2].split(":")[0]
|
||||
tracker["tag"] = self.util.check_for_attribute(
|
||||
self.data, "tag", parent="tracker", subparent=default_tag, default=default_tag, var_type="list"
|
||||
)
|
||||
if isinstance(tracker["tag"], str):
|
||||
tracker["tag"] = [tracker["tag"]]
|
||||
try:
|
||||
self.data["tracker"][default_tag]["tag"] = [default_tag]
|
||||
except Exception:
|
||||
self.data["tracker"][default_tag] = {"tag": [default_tag]}
|
||||
e = f'No tags matched for {tracker["url"]}. Please check your config.yml file. Setting tag to {default_tag}'
|
||||
self.notify(e, "Tag", False)
|
||||
logger.warning(e)
|
||||
return tracker
|
||||
|
||||
# Get category from config file based on path provided
|
||||
def get_category(self, path):
|
||||
category = ""
|
||||
path = os.path.join(path, "")
|
||||
if "cat" in self.data and self.data["cat"] is not None:
|
||||
cat_path = self.data["cat"]
|
||||
for cat, save_path in cat_path.items():
|
||||
if os.path.join(save_path, "") == path:
|
||||
category = cat
|
||||
break
|
||||
|
||||
if not category:
|
||||
default_cat = path.split(os.sep)[-2]
|
||||
category = str(default_cat)
|
||||
self.util.check_for_attribute(self.data, default_cat, parent="cat", default=path)
|
||||
self.data["cat"][str(default_cat)] = path
|
||||
e = f"No categories matched for the save path {path}. Check your config.yml file. - Setting category to {default_cat}"
|
||||
self.notify(e, "Category", False)
|
||||
logger.warning(e)
|
||||
return category
|
||||
|
||||
# Empty old files from recycle bin or orphaned
|
||||
def cleanup_dirs(self, location):
|
||||
num_del = 0
|
||||
|
|
|
|||
3
modules/core/__init__.py
Normal file
3
modules/core/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
"""
|
||||
modules.core contains all the core functions of qbit_manage such as updating categories/tags etc..
|
||||
"""
|
||||
76
modules/core/category.py
Normal file
76
modules/core/category.py
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
from qbittorrentapi import Conflict409Error
|
||||
|
||||
from modules import util
|
||||
|
||||
logger = util.logger
|
||||
|
||||
|
||||
class Category:
|
||||
def __init__(self, qbit_manager):
|
||||
self.qbt = qbit_manager
|
||||
self.config = qbit_manager.config
|
||||
self.client = qbit_manager.client
|
||||
self.stats = 0
|
||||
|
||||
self.category()
|
||||
|
||||
def category(self):
|
||||
"""Update category for torrents that don't have any category defined and returns total number categories updated"""
|
||||
logger.separator("Updating Categories", space=False, border=False)
|
||||
torrent_list = self.qbt.get_torrents({"category": "", "status_filter": "completed"})
|
||||
for torrent in torrent_list:
|
||||
new_cat = self.qbt.get_category(torrent.save_path)
|
||||
self.update_cat(torrent, new_cat, False)
|
||||
|
||||
# Change categories
|
||||
if self.config.cat_change:
|
||||
for old_cat in self.config.cat_change:
|
||||
torrent_list = self.qbt.get_torrents({"category": old_cat, "status_filter": "completed"})
|
||||
for torrent in torrent_list:
|
||||
new_cat = self.config.cat_change[old_cat]
|
||||
self.update_cat(torrent, new_cat, True)
|
||||
|
||||
if self.stats >= 1:
|
||||
logger.print_line(
|
||||
f"{'Did not update' if self.config.dry_run else 'Updated'} {self.stats} new categories.", self.config.loglevel
|
||||
)
|
||||
else:
|
||||
logger.print_line("No new torrents to categorize.", self.config.loglevel)
|
||||
|
||||
def update_cat(self, torrent, new_cat, cat_change):
|
||||
"""Update category based on the torrent information"""
|
||||
tracker = self.qbt.get_tags(torrent.trackers)
|
||||
old_cat = torrent.category
|
||||
if not self.config.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:
|
||||
ex = logger.print_line(
|
||||
f'Existing category "{new_cat}" not found for save path {torrent.save_path}, category will be created.',
|
||||
self.config.loglevel,
|
||||
)
|
||||
self.config.notify(ex, "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 += logger.print_line(logger.insert_space(f"Torrent Name: {torrent.name}", 3), self.config.loglevel)
|
||||
if cat_change:
|
||||
body += logger.print_line(logger.insert_space(f"Old Category: {old_cat}", 3), self.config.loglevel)
|
||||
title = "Moving Categories"
|
||||
else:
|
||||
title = "Updating Categories"
|
||||
body += logger.print_line(logger.insert_space(f"New Category: {new_cat}", 3), self.config.loglevel)
|
||||
body += logger.print_line(logger.insert_space(f'Tracker: {tracker["url"]}', 8), self.config.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)
|
||||
self.stats += 1
|
||||
124
modules/core/cross_seed.py
Normal file
124
modules/core/cross_seed.py
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
import os
|
||||
from collections import Counter
|
||||
|
||||
from modules import util
|
||||
|
||||
logger = util.logger
|
||||
|
||||
|
||||
class CrossSeed:
|
||||
def __init__(self, qbit_manager):
|
||||
self.qbt = qbit_manager
|
||||
self.config = qbit_manager.config
|
||||
self.client = qbit_manager.client
|
||||
self.stats_added = 0
|
||||
self.stats_tagged = 0
|
||||
|
||||
self.cross_seed()
|
||||
|
||||
def cross_seed(self):
|
||||
"""Move torrents from cross seed directory to correct save directory."""
|
||||
logger.separator("Checking for Cross-Seed Torrents", space=False, border=False)
|
||||
# List of categories for all torrents moved
|
||||
categories = []
|
||||
|
||||
# Only get torrent files
|
||||
cs_files = [f for f in os.listdir(self.config.cross_seed_dir) if f.endswith("torrent")]
|
||||
dir_cs = self.config.cross_seed_dir
|
||||
dir_cs_out = os.path.join(dir_cs, "qbit_manage_added")
|
||||
os.makedirs(dir_cs_out, exist_ok=True)
|
||||
for file in cs_files:
|
||||
tr_name = file.split("]", 2)[2].split(".torrent")[0]
|
||||
t_tracker = file.split("]", 2)[1][1:]
|
||||
# Substring Key match in dictionary (used because t_name might not match exactly with self.qbt.torrentinfo key)
|
||||
# Returned the dictionary of filtered item
|
||||
torrentdict_file = dict(filter(lambda item: tr_name in item[0], self.qbt.torrentinfo.items()))
|
||||
if torrentdict_file:
|
||||
# Get the exact torrent match name from self.qbt.torrentinfo
|
||||
t_name = next(iter(torrentdict_file))
|
||||
dest = os.path.join(self.qbt.torrentinfo[t_name]["save_path"], "")
|
||||
src = os.path.join(dir_cs, file)
|
||||
dir_cs_out = os.path.join(dir_cs, "qbit_manage_added", file)
|
||||
category = self.qbt.global_max_ratioget_category(dest)
|
||||
# Only add cross-seed torrent if original torrent is complete
|
||||
if self.qbt.torrentinfo[t_name]["is_complete"]:
|
||||
categories.append(category)
|
||||
body = []
|
||||
body += logger.print_line(
|
||||
f"{'Not Adding' if self.config.dry_run else 'Adding'} to qBittorrent:", self.config.loglevel
|
||||
)
|
||||
body += logger.print_line(logger.insert_space(f"Torrent Name: {t_name}", 3), self.config.loglevel)
|
||||
body += logger.print_line(logger.insert_space(f"Category: {category}", 7), self.config.loglevel)
|
||||
body += logger.print_line(logger.insert_space(f"Save_Path: {dest}", 6), self.config.loglevel)
|
||||
body += logger.print_line(logger.insert_space(f"Tracker: {t_tracker}", 8), self.config.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,
|
||||
"torrent_tag": "cross-seed",
|
||||
"torrent_tracker": t_tracker,
|
||||
}
|
||||
self.config.send_notifications(attr)
|
||||
self.stats_added += 1
|
||||
if not self.config.dry_run:
|
||||
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:
|
||||
logger.print_line(f"Found {t_name} in {dir_cs} but original torrent is not complete.", self.config.loglevel)
|
||||
logger.print_line("Not adding to qBittorrent", self.config.loglevel)
|
||||
else:
|
||||
error = f"{t_name} not found in torrents. Cross-seed Torrent not added to qBittorrent."
|
||||
if self.config.dry_run:
|
||||
logger.print_line(error, self.config.loglevel)
|
||||
else:
|
||||
logger.print_line(error, "WARNING")
|
||||
self.config.notify(error, "cross-seed", False)
|
||||
# Tag missing cross-seed torrents tags
|
||||
for torrent in self.qbt.torrent_list:
|
||||
t_name = torrent.name
|
||||
t_cat = torrent.category
|
||||
if (
|
||||
"cross-seed" not in torrent.tags
|
||||
and self.qbt.torrentinfo[t_name]["count"] > 1
|
||||
and self.qbt.torrentinfo[t_name]["first_hash"] != torrent.hash
|
||||
):
|
||||
tracker = self.qbt.get_tags(torrent.trackers)
|
||||
self.stats_tagged += 1
|
||||
body = logger.print_line(
|
||||
f"{'Not Adding' if self.config.dry_run else 'Adding'} 'cross-seed' tag to {t_name}", self.config.loglevel
|
||||
)
|
||||
attr = {
|
||||
"function": "tag_cross_seed",
|
||||
"title": "Tagging Cross-Seed Torrent",
|
||||
"body": body,
|
||||
"torrent_name": t_name,
|
||||
"torrent_category": t_cat,
|
||||
"torrent_tag": "cross-seed",
|
||||
"torrent_tracker": tracker,
|
||||
}
|
||||
self.config.send_notifications(attr)
|
||||
if not self.config.dry_run:
|
||||
torrent.add_tags(tags="cross-seed")
|
||||
|
||||
numcategory = Counter(categories)
|
||||
for cat in numcategory:
|
||||
if numcategory[cat] > 0:
|
||||
logger.print_line(
|
||||
f"{numcategory[cat]} {cat} cross-seed .torrents {'not added' if self.config.dry_run else 'added'}.",
|
||||
self.config.loglevel,
|
||||
)
|
||||
if self.stats_added > 0:
|
||||
logger.print_line(
|
||||
f"Total {self.stats_added} cross-seed .torrents {'not added' if self.config.dry_run else 'added'}.",
|
||||
self.config.loglevel,
|
||||
)
|
||||
if self.stats_tagged > 0:
|
||||
logger.print_line(
|
||||
f"Total {self.stats_tagged} cross-seed .torrents {'not added' if self.config.dry_run else 'added'}.",
|
||||
self.config.loglevel,
|
||||
)
|
||||
115
modules/core/recheck.py
Normal file
115
modules/core/recheck.py
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
from datetime import timedelta
|
||||
|
||||
from modules import util
|
||||
|
||||
logger = util.logger
|
||||
|
||||
|
||||
class ReCheck:
|
||||
def __init__(self, qbit_manager):
|
||||
self.qbt = qbit_manager
|
||||
self.config = qbit_manager.config
|
||||
self.client = qbit_manager.client
|
||||
self.stats_resumed = 0
|
||||
self.stats_rechecked = 0
|
||||
|
||||
self.recheck()
|
||||
|
||||
def recheck(self):
|
||||
"""Function used to recheck paused torrents sorted by size and resume torrents that are completed"""
|
||||
if self.config.commands["recheck"]:
|
||||
logger.separator("Rechecking Paused Torrents", space=False, border=False)
|
||||
# sort by size and paused
|
||||
torrent_list = self.qbt.get_torrents({"status_filter": "paused", "sort": "size"})
|
||||
if torrent_list:
|
||||
for torrent in torrent_list:
|
||||
tracker = self.qbt.get_tags(torrent.trackers)
|
||||
# Resume torrent if completed
|
||||
if torrent.progress == 1:
|
||||
if torrent.max_ratio < 0 and torrent.max_seeding_time < 0:
|
||||
self.stats_resumed += 1
|
||||
body = logger.print_line(
|
||||
f"{'Not Resuming' if self.config.dry_run else 'Resuming'} [{tracker['tag']}] - {torrent.name}",
|
||||
self.config.loglevel,
|
||||
)
|
||||
attr = {
|
||||
"function": "recheck",
|
||||
"title": "Resuming Torrent",
|
||||
"body": body,
|
||||
"torrent_name": torrent.name,
|
||||
"torrent_category": torrent.category,
|
||||
"torrent_tracker": tracker["url"],
|
||||
"notifiarr_indexer": tracker["notifiarr"],
|
||||
}
|
||||
self.config.send_notifications(attr)
|
||||
if not self.config.dry_run:
|
||||
torrent.resume()
|
||||
else:
|
||||
# Check to see if torrent meets AutoTorrentManagement criteria
|
||||
logger.debug("DEBUG: Torrent to see if torrent meets AutoTorrentManagement Criteria")
|
||||
logger.debug(logger.insert_space(f"- Torrent Name: {torrent.name}", 2))
|
||||
logger.debug(
|
||||
logger.insert_space(f"-- Ratio vs Max Ratio: {torrent.ratio:.2f} < {torrent.max_ratio:.2f}", 4)
|
||||
)
|
||||
logger.debug(
|
||||
logger.insert_space(
|
||||
f"-- Seeding Time vs Max Seed Time: {timedelta(seconds=torrent.seeding_time)} < "
|
||||
f"{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))
|
||||
)
|
||||
):
|
||||
self.stats_resumed += 1
|
||||
body = logger.print_line(
|
||||
f"{'Not Resuming' if self.config.dry_run else 'Resuming'} [{tracker['tag']}] - "
|
||||
f"{torrent.name}",
|
||||
self.config.loglevel,
|
||||
)
|
||||
attr = {
|
||||
"function": "recheck",
|
||||
"title": "Resuming Torrent",
|
||||
"body": body,
|
||||
"torrent_name": torrent.name,
|
||||
"torrent_category": torrent.category,
|
||||
"torrent_tracker": tracker["url"],
|
||||
"notifiarr_indexer": tracker["notifiarr"],
|
||||
}
|
||||
self.config.send_notifications(attr)
|
||||
if not self.config.dry_run:
|
||||
torrent.resume()
|
||||
# Recheck
|
||||
elif (
|
||||
torrent.progress == 0
|
||||
and self.qbt.torrentinfo[torrent.name]["is_complete"]
|
||||
and not torrent.state_enum.is_checking
|
||||
):
|
||||
self.stats_rechecked += 1
|
||||
body = logger.print_line(
|
||||
f"{'Not Rechecking' if self.config.dry_run else 'Rechecking'} [{tracker['tag']}] - {torrent.name}",
|
||||
self.config.loglevel,
|
||||
)
|
||||
attr = {
|
||||
"function": "recheck",
|
||||
"title": "Rechecking Torrent",
|
||||
"body": body,
|
||||
"torrent_name": torrent.name,
|
||||
"torrent_category": torrent.category,
|
||||
"torrent_tracker": tracker["url"],
|
||||
"notifiarr_indexer": tracker["notifiarr"],
|
||||
}
|
||||
self.config.send_notifications(attr)
|
||||
if not self.config.dry_run:
|
||||
torrent.recheck()
|
||||
102
modules/core/remove_orphaned.py
Normal file
102
modules/core/remove_orphaned.py
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
import os
|
||||
from fnmatch import fnmatch
|
||||
|
||||
from modules import util
|
||||
|
||||
logger = util.logger
|
||||
|
||||
|
||||
class RemoveOrphaned:
|
||||
def __init__(self, qbit_manager):
|
||||
self.qbt = qbit_manager
|
||||
self.config = qbit_manager.config
|
||||
self.client = qbit_manager.client
|
||||
self.stats = 0
|
||||
|
||||
self.remote_dir = qbit_manager.config.remote_dir
|
||||
self.root_dir = qbit_manager.config.root_dir
|
||||
self.orphaned_dir = qbit_manager.config.orphaned_dir
|
||||
|
||||
self.rem_orphaned()
|
||||
|
||||
def rem_orphaned(self):
|
||||
"""Remove orphaned files from remote directory"""
|
||||
self.stats = 0
|
||||
logger.separator("Checking for Orphaned Files", space=False, border=False)
|
||||
torrent_files = []
|
||||
root_files = []
|
||||
orphaned_files = []
|
||||
excluded_orphan_files = []
|
||||
orphaned_parent_path = set()
|
||||
|
||||
if self.remote_dir != self.root_dir:
|
||||
root_files = [
|
||||
os.path.join(path.replace(self.remote_dir, self.root_dir), name)
|
||||
for path, subdirs, files in os.walk(self.remote_dir)
|
||||
for name in files
|
||||
if self.orphaned_dir.replace(self.remote_dir, self.root_dir) not in path
|
||||
]
|
||||
else:
|
||||
root_files = [
|
||||
os.path.join(path, name)
|
||||
for path, subdirs, files in os.walk(self.root_dir)
|
||||
for name in files
|
||||
if self.orphaned_dir.replace(self.root_dir, self.remote_dir) not in path
|
||||
]
|
||||
|
||||
# Get an updated list of torrents
|
||||
torrent_list = self.qbt.get_torrents({"sort": "added_on"})
|
||||
for torrent in torrent_list:
|
||||
for file in torrent.files:
|
||||
fullpath = os.path.join(torrent.save_path, file.name)
|
||||
# Replace fullpath with \\ if qbm is running in docker (linux) but qbt is on windows
|
||||
fullpath = fullpath.replace(r"/", "\\") if ":\\" in fullpath else fullpath
|
||||
torrent_files.append(fullpath)
|
||||
|
||||
orphaned_files = set(root_files) - set(torrent_files)
|
||||
orphaned_files = sorted(orphaned_files)
|
||||
|
||||
if self.config.orphaned["exclude_patterns"]:
|
||||
exclude_patterns = self.config.orphaned["exclude_patterns"]
|
||||
excluded_orphan_files = [
|
||||
file
|
||||
for file in orphaned_files
|
||||
for exclude_pattern in exclude_patterns
|
||||
if fnmatch(file, exclude_pattern.replace(self.remote_dir, self.root_dir))
|
||||
]
|
||||
|
||||
orphaned_files = set(orphaned_files) - set(excluded_orphan_files)
|
||||
|
||||
if orphaned_files:
|
||||
os.makedirs(self.orphaned_dir, exist_ok=True)
|
||||
body = []
|
||||
num_orphaned = len(orphaned_files)
|
||||
logger.print_line(f"{num_orphaned} Orphaned files found", self.config.loglevel)
|
||||
body += logger.print_line("\n".join(orphaned_files), self.config.loglevel)
|
||||
body += logger.print_line(
|
||||
f"{'Did not move' if self.config.dry_run else 'Moved'} {num_orphaned} Orphaned files "
|
||||
f"to {self.orphaned_dir.replace(self.remote_dir,self.root_dir)}",
|
||||
self.config.loglevel,
|
||||
)
|
||||
|
||||
attr = {
|
||||
"function": "rem_orphaned",
|
||||
"title": f"Removing {num_orphaned} Orphaned Files",
|
||||
"body": "\n".join(body),
|
||||
"orphaned_files": list(orphaned_files),
|
||||
"orphaned_directory": self.orphaned_dir.replace(self.remote_dir, self.root_dir),
|
||||
"total_orphaned_files": num_orphaned,
|
||||
}
|
||||
self.config.send_notifications(attr)
|
||||
# Delete empty directories after moving orphan files
|
||||
logger.info("Cleaning up any empty directories...")
|
||||
if not self.config.dry_run:
|
||||
for file in orphaned_files:
|
||||
src = file.replace(self.root_dir, self.remote_dir)
|
||||
dest = os.path.join(self.orphaned_dir, file.replace(self.root_dir, ""))
|
||||
util.move_files(src, dest, True)
|
||||
orphaned_parent_path.add(os.path.dirname(file).replace(self.root_dir, self.remote_dir))
|
||||
for parent_path in orphaned_parent_path:
|
||||
util.remove_empty_directories(parent_path, "**/*")
|
||||
else:
|
||||
logger.print_line("No Orphaned Files found.", self.config.loglevel)
|
||||
216
modules/core/remove_unregistered.py
Normal file
216
modules/core/remove_unregistered.py
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
from qbittorrentapi import NotFound404Error
|
||||
from qbittorrentapi import TrackerStatus
|
||||
|
||||
from modules import util
|
||||
from modules.util import list_in_text
|
||||
from modules.util import TorrentMessages
|
||||
|
||||
logger = util.logger
|
||||
|
||||
|
||||
class RemoveUnregistered:
|
||||
def __init__(self, qbit_manager):
|
||||
self.qbt = qbit_manager
|
||||
self.config = qbit_manager.config
|
||||
self.client = qbit_manager.client
|
||||
self.stats_deleted = 0
|
||||
self.stats_deleted_contents = 0
|
||||
self.stats_tagged = 0
|
||||
self.stats_untagged = 0
|
||||
self.tor_error_summary = ""
|
||||
self.tag_error = self.config.tracker_error_tag
|
||||
self.cfg_rem_unregistered = self.config.commands["rem_unregistered"]
|
||||
self.cfg_tag_error = self.config.commands["tag_tracker_error"]
|
||||
|
||||
tag_error_msg = "Tagging Torrents with Tracker Errors" if self.cfg_tag_error else ""
|
||||
rem_unregistered_msg = "Removing Unregistered Torrents" if self.cfg_rem_unregistered else ""
|
||||
|
||||
if tag_error_msg and rem_unregistered_msg:
|
||||
message = f"{tag_error_msg} and {rem_unregistered_msg}"
|
||||
elif tag_error_msg:
|
||||
message = tag_error_msg
|
||||
elif rem_unregistered_msg:
|
||||
message = rem_unregistered_msg
|
||||
|
||||
if message:
|
||||
logger.separator(message, space=False, border=False)
|
||||
|
||||
self.rem_unregistered()
|
||||
|
||||
def remove_previous_errors(self):
|
||||
"""Removes any previous torrents that were tagged as an error but are now working."""
|
||||
for torrent in self.qbt.torrentvalid:
|
||||
check_tags = util.get_list(torrent.tags)
|
||||
# Remove any error torrents Tags that are no longer unreachable.
|
||||
if self.tag_error in check_tags:
|
||||
tracker = self.qbt.get_tags(torrent.trackers)
|
||||
self.stats_untagged += 1
|
||||
body = []
|
||||
body += logger.print_line(
|
||||
f"Previous Tagged {self.tag_error} torrent currently has a working tracker.", self.config.loglevel
|
||||
)
|
||||
body += logger.print_line(logger.insert_space(f"Torrent Name: {torrent.name}", 3), self.config.loglevel)
|
||||
body += logger.print_line(logger.insert_space(f"Removed Tag: {self.tag_error}", 4), self.config.loglevel)
|
||||
body += logger.print_line(logger.insert_space(f'Tracker: {tracker["url"]}', 8), self.config.loglevel)
|
||||
if not self.config.dry_run:
|
||||
torrent.remove_tags(tags=self.tag_error)
|
||||
attr = {
|
||||
"function": "untag_tracker_error",
|
||||
"title": "Untagging Tracker Error Torrent",
|
||||
"body": "\n".join(body),
|
||||
"torrent_name": torrent.name,
|
||||
"torrent_category": torrent.category,
|
||||
"torrent_tag": self.tag_error,
|
||||
"torrent_tracker": tracker["url"],
|
||||
"notifiarr_indexer": tracker["notifiarr"],
|
||||
}
|
||||
self.config.send_notifications(attr)
|
||||
|
||||
def check_for_unregistered_torrents_using_bhd_api(self, tracker, msg_up, torrent_hash):
|
||||
"""
|
||||
Checks if a torrent is unregistered using the BHD API if the tracker is BHD.
|
||||
"""
|
||||
if (
|
||||
"tracker.beyond-hd.me" in tracker["url"]
|
||||
and self.config.beyond_hd is not None
|
||||
and not list_in_text(msg_up, TorrentMessages.IGNORE_MSGS)
|
||||
):
|
||||
json = {"info_hash": torrent_hash}
|
||||
response = self.config.beyond_hd.search(json)
|
||||
if response["total_results"] == 0:
|
||||
return True
|
||||
return False
|
||||
|
||||
def process_torrent_issues(self):
|
||||
for torrent in self.qbt.torrentissue:
|
||||
self.t_name = torrent.name
|
||||
self.t_cat = self.qbt.torrentinfo[self.t_name]["Category"]
|
||||
self.t_msg = self.qbt.torrentinfo[self.t_name]["msg"]
|
||||
self.t_status = self.qbt.torrentinfo[self.t_name]["status"]
|
||||
check_tags = util.get_list(torrent.tags)
|
||||
try:
|
||||
for trk in torrent.trackers:
|
||||
if trk.url.startswith("http"):
|
||||
tracker = self.qbt.get_tags([trk])
|
||||
msg_up = trk.msg.upper()
|
||||
msg = trk.msg
|
||||
if TrackerStatus(trk.status) == TrackerStatus.NOT_WORKING:
|
||||
# Tag any error torrents
|
||||
if self.cfg_tag_error and self.tag_error not in check_tags:
|
||||
self.tag_tracker_error(msg, tracker, torrent)
|
||||
# Check for unregistered torrents
|
||||
if self.cfg_rem_unregistered:
|
||||
if list_in_text(msg_up, TorrentMessages.UNREGISTERED_MSGS) and not list_in_text(
|
||||
msg_up, TorrentMessages.IGNORE_MSGS
|
||||
):
|
||||
self.del_unregistered(msg, tracker, torrent)
|
||||
break
|
||||
else:
|
||||
if self.check_for_unregistered_torrents_using_bhd_api(tracker, msg_up, torrent.hash):
|
||||
self.del_unregistered(msg, tracker, torrent)
|
||||
break
|
||||
|
||||
except NotFound404Error:
|
||||
continue
|
||||
except Exception as ex:
|
||||
logger.stacktrace()
|
||||
self.config.notify(ex, "Remove Unregistered Torrents", False)
|
||||
logger.error(f"Remove Unregistered Torrents Error: {ex}")
|
||||
|
||||
def rem_unregistered(self):
|
||||
"""Remove torrents with unregistered trackers."""
|
||||
self.remove_previous_errors()
|
||||
self.process_torrent_issues()
|
||||
if self.cfg_rem_unregistered:
|
||||
if self.stats_deleted >= 1 or self.stats_deleted_contents >= 1:
|
||||
if self.stats_deleted >= 1:
|
||||
logger.print_line(
|
||||
f"{'Did not delete' if self.config.dry_run else 'Deleted'} {self.stats_deleted} "
|
||||
f".torrent{'s' if self.stats_deleted > 1 else ''} but not content files.",
|
||||
self.config.loglevel,
|
||||
)
|
||||
if self.stats_deleted_contents >= 1:
|
||||
logger.print_line(
|
||||
f"{'Did not delete' if self.config.dry_run else 'Deleted'} {self.stats_deleted_contents} "
|
||||
f".torrent{'s' if self.stats_deleted_contents > 1 else ''} AND content files.",
|
||||
self.config.loglevel,
|
||||
)
|
||||
else:
|
||||
logger.print_line("No unregistered torrents found.", self.config.loglevel)
|
||||
if self.stats_untagged >= 1:
|
||||
logger.print_line(
|
||||
f"{'Did not delete' if self.config.dry_run else 'Deleted'} {self.tag_error} tags for {self.stats_untagged} "
|
||||
f".torrent{'s.' if self.stats_untagged > 1 else '.'}",
|
||||
self.config.loglevel,
|
||||
)
|
||||
if self.stats_tagged >= 1:
|
||||
logger.separator(
|
||||
f"{self.stats_tagged} Torrents with tracker errors found",
|
||||
space=False,
|
||||
border=False,
|
||||
loglevel=self.config.loglevel,
|
||||
)
|
||||
logger.print_line(self.tor_error_summary.rstrip(), self.config.loglevel)
|
||||
|
||||
def tag_tracker_error(self, msg, tracker, torrent):
|
||||
"""Tags any trackers with errors"""
|
||||
tor_error = ""
|
||||
tor_error += logger.insert_space(f"Torrent Name: {self.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: {self.tag_error}", 6) + "\n"
|
||||
self.tor_error_summary += tor_error
|
||||
self.stats_tagged += 1
|
||||
attr = {
|
||||
"function": "tag_tracker_error",
|
||||
"title": "Tag Tracker Error Torrents",
|
||||
"body": tor_error,
|
||||
"torrent_name": self.t_name,
|
||||
"torrent_category": self.t_cat,
|
||||
"torrent_tag": self.tag_error,
|
||||
"torrent_status": msg,
|
||||
"torrent_tracker": tracker["url"],
|
||||
"notifiarr_indexer": tracker["notifiarr"],
|
||||
}
|
||||
self.config.send_notifications(attr)
|
||||
if not self.config.dry_run:
|
||||
torrent.add_tags(tags=self.tag_error)
|
||||
|
||||
def del_unregistered(self, msg, tracker, torrent):
|
||||
"""Deletes unregistered torrents"""
|
||||
body = []
|
||||
body += logger.print_line(logger.insert_space(f"Torrent Name: {self.t_name}", 3), self.config.loglevel)
|
||||
body += logger.print_line(logger.insert_space(f"Status: {msg}", 9), self.config.loglevel)
|
||||
body += logger.print_line(logger.insert_space(f'Tracker: {tracker["url"]}', 8), self.config.loglevel)
|
||||
attr = {
|
||||
"function": "rem_unregistered",
|
||||
"title": "Removing Unregistered Torrents",
|
||||
"torrent_name": self.t_name,
|
||||
"torrent_category": self.t_cat,
|
||||
"torrent_status": msg,
|
||||
"torrent_tracker": tracker["url"],
|
||||
"notifiarr_indexer": tracker["notifiarr"],
|
||||
}
|
||||
if self.qbt.torrentinfo[self.t_name]["count"] > 1:
|
||||
# Checks if any of the original torrents are working
|
||||
if "" in self.t_msg or 2 in self.t_status:
|
||||
attr["torrents_deleted_and_contents"] = False
|
||||
if not self.config.dry_run:
|
||||
self.qbt.tor_delete_recycle(torrent, attr)
|
||||
body += logger.print_line(logger.insert_space("Deleted .torrent but NOT content files.", 8), self.config.loglevel)
|
||||
self.stats_deleted += 1
|
||||
else:
|
||||
attr["torrents_deleted_and_contents"] = True
|
||||
if not self.config.dry_run:
|
||||
self.qbt.tor_delete_recycle(torrent, attr)
|
||||
body += logger.print_line(logger.insert_space("Deleted .torrent AND content files.", 8), self.config.loglevel)
|
||||
self.stats_deleted_contents += 1
|
||||
else:
|
||||
attr["torrents_deleted_and_contents"] = True
|
||||
if not self.config.dry_run:
|
||||
self.qbt.tor_delete_recycle(torrent, attr)
|
||||
body += logger.print_line(logger.insert_space("Deleted .torrent AND content files.", 8), self.config.loglevel)
|
||||
self.stats_deleted_contents += 1
|
||||
attr["body"] = "\n".join(body)
|
||||
self.config.send_notifications(attr)
|
||||
self.qbt.torrentinfo[self.t_name]["count"] -= 1
|
||||
350
modules/core/tag_nohardlinks.py
Normal file
350
modules/core/tag_nohardlinks.py
Normal file
|
|
@ -0,0 +1,350 @@
|
|||
import os
|
||||
|
||||
from modules import util
|
||||
|
||||
logger = util.logger
|
||||
|
||||
|
||||
class TagNoHardLinks:
|
||||
def __init__(self, qbit_manager):
|
||||
self.qbt = qbit_manager
|
||||
self.config = qbit_manager.config
|
||||
self.client = qbit_manager.client
|
||||
self.stats_tagged = 0 # counter for the number of torrents that has no hardlinks
|
||||
self.stats_untagged = 0 # counter for number of torrents that previously had no hardlinks but now have hardlinks
|
||||
self.stats_deleted = 0 # counter for the number of torrents that has no hardlinks and \
|
||||
# meets the criteria for ratio limit/seed limit for deletion
|
||||
self.stats_deleted_contents = 0 # counter for the number of torrents that has no hardlinks and \
|
||||
# meets the criteria for ratio limit/seed limit for deletion including contents
|
||||
|
||||
self.tdel_dict = {} # dictionary to track the torrent names and content path that meet the deletion criteria
|
||||
self.root_dir = qbit_manager.config.root_dir
|
||||
self.remote_dir = qbit_manager.config.remote_dir
|
||||
self.nohardlinks = qbit_manager.config.nohardlinks
|
||||
self.nohardlinks_tag = qbit_manager.config.nohardlinks_tag
|
||||
|
||||
self.tag_nohardlinks()
|
||||
|
||||
def add_tag_no_hl(self, torrent, tracker, category, max_ratio, max_seeding_time, add_tag=True):
|
||||
"""Add tag nohardlinks_tag to torrents with no hardlinks"""
|
||||
body = []
|
||||
body.append(logger.insert_space(f"Torrent Name: {torrent.name}", 3))
|
||||
if add_tag:
|
||||
body.append(logger.insert_space(f"Added Tag: {self.nohardlinks_tag}", 6))
|
||||
title = "Tagging Torrents with No Hardlinks"
|
||||
else:
|
||||
title = "Changing Share Ratio of Torrents with No Hardlinks"
|
||||
body.append(logger.insert_space(f'Tracker: {tracker["url"]}', 8))
|
||||
body_tags_and_limits = self.qbt.set_tags_and_limits(
|
||||
torrent,
|
||||
max_ratio,
|
||||
max_seeding_time,
|
||||
self.nohardlinks[category]["limit_upload_speed"],
|
||||
tags=self.nohardlinks_tag,
|
||||
do_print=False,
|
||||
)
|
||||
if body_tags_and_limits or add_tag:
|
||||
self.stats_tagged += 1
|
||||
# Resume torrent if it was paused now that the share limit has changed
|
||||
if torrent.state_enum.is_complete and self.nohardlinks[category]["resume_torrent_after_untagging_noHL"]:
|
||||
if not self.config.dry_run:
|
||||
torrent.resume()
|
||||
body.extend(body_tags_and_limits)
|
||||
for rcd in body:
|
||||
logger.print_line(rcd, self.config.loglevel)
|
||||
attr = {
|
||||
"function": "tag_nohardlinks",
|
||||
"title": title,
|
||||
"body": "\n".join(body),
|
||||
"torrent_name": torrent.name,
|
||||
"torrent_category": torrent.category,
|
||||
"torrent_tag": self.nohardlinks_tag,
|
||||
"torrent_tracker": tracker["url"],
|
||||
"notifiarr_indexer": tracker["notifiarr"],
|
||||
"torrent_max_ratio": max_ratio,
|
||||
"torrent_max_seeding_time": max_seeding_time,
|
||||
"torrent_limit_upload_speed": self.nohardlinks[category]["limit_upload_speed"],
|
||||
}
|
||||
self.config.send_notifications(attr)
|
||||
|
||||
def cleanup_tagged_torrents_with_no_hardlinks(self, category):
|
||||
"""Delete any tagged torrents that meet noHL criteria"""
|
||||
# loop through torrent list again for cleanup purposes
|
||||
if self.nohardlinks[category]["cleanup"]:
|
||||
torrent_list = self.qbt.get_torrents({"category": category, "status_filter": "completed"})
|
||||
for torrent in torrent_list:
|
||||
t_name = torrent.name
|
||||
t_hash = torrent.hash
|
||||
if t_hash in self.tdel_dict and self.nohardlinks_tag in torrent.tags:
|
||||
t_count = self.qbt.torrentinfo[t_name]["count"]
|
||||
t_msg = self.qbt.torrentinfo[t_name]["msg"]
|
||||
t_status = self.qbt.torrentinfo[t_name]["status"]
|
||||
# Double check that the content path is the same before we delete anything
|
||||
if torrent["content_path"].replace(self.root_dir, self.remote_dir) == self.tdel_dict[t_hash]["content_path"]:
|
||||
tracker = self.qbt.get_tags(torrent.trackers)
|
||||
body = []
|
||||
body += logger.print_line(logger.insert_space(f"Torrent Name: {t_name}", 3), self.config.loglevel)
|
||||
body += logger.print_line(logger.insert_space(f'Tracker: {tracker["url"]}', 8), self.config.loglevel)
|
||||
body += logger.print_line(self.tdel_dict[t_hash]["body"], self.config.loglevel)
|
||||
body += logger.print_line(
|
||||
logger.insert_space("Cleanup: True [No hardlinks found and meets Share Limits.]", 8),
|
||||
self.config.loglevel,
|
||||
)
|
||||
attr = {
|
||||
"function": "cleanup_tag_nohardlinks",
|
||||
"title": "Removing NoHL Torrents and meets Share Limits",
|
||||
"torrent_name": t_name,
|
||||
"torrent_category": torrent.category,
|
||||
"cleanup": "True",
|
||||
"torrent_tracker": tracker["url"],
|
||||
"notifiarr_indexer": tracker["notifiarr"],
|
||||
}
|
||||
if os.path.exists(torrent["content_path"].replace(self.root_dir, self.remote_dir)):
|
||||
# Checks if any of the original torrents are working
|
||||
if t_count > 1 and ("" in t_msg or 2 in t_status):
|
||||
self.stats_deleted += 1
|
||||
attr["torrents_deleted_and_contents"] = False
|
||||
if not self.config.dry_run:
|
||||
self.qbt.tor_delete_recycle(torrent, attr)
|
||||
body += logger.print_line(
|
||||
logger.insert_space("Deleted .torrent but NOT content files.", 8),
|
||||
self.config.loglevel,
|
||||
)
|
||||
else:
|
||||
self.stats_deleted_contents += 1
|
||||
attr["torrents_deleted_and_contents"] = True
|
||||
if not self.config.dry_run:
|
||||
self.qbt.tor_delete_recycle(torrent, attr)
|
||||
body += logger.print_line(
|
||||
logger.insert_space("Deleted .torrent AND content files.", 8), self.config.loglevel
|
||||
)
|
||||
else:
|
||||
self.stats_deleted += 1
|
||||
attr["torrents_deleted_and_contents"] = False
|
||||
if not self.config.dry_run:
|
||||
self.qbt.tor_delete_recycle(torrent, attr)
|
||||
body += logger.print_line(
|
||||
logger.insert_space("Deleted .torrent but NOT content files.", 8), self.config.loglevel
|
||||
)
|
||||
attr["body"] = "\n".join(body)
|
||||
self.config.send_notifications(attr)
|
||||
self.qbt.torrentinfo[t_name]["count"] -= 1
|
||||
|
||||
def check_previous_nohardlinks_tagged_torrents(self, has_nohardlinks, torrent, tracker, category):
|
||||
"""
|
||||
Checks for any previous torrents that were tagged with the nohardlinks tag and have since had hardlinks added.
|
||||
If any are found, the nohardlinks tag is removed from the torrent and the tracker or global share limits are restored.
|
||||
If the torrent is complete and the option to resume after untagging is enabled, the torrent is resumed.
|
||||
"""
|
||||
if not (has_nohardlinks) and (self.nohardlinks_tag in torrent.tags):
|
||||
self.stats_untagged += 1
|
||||
body = []
|
||||
body += logger.print_line(
|
||||
f"Previous Tagged {self.nohardlinks_tag} " f"Torrent Name: {torrent.name} has hardlinks found now.",
|
||||
self.config.loglevel,
|
||||
)
|
||||
body += logger.print_line(logger.insert_space(f"Removed Tag: {self.nohardlinks_tag}", 6), self.config.loglevel)
|
||||
body += logger.print_line(logger.insert_space(f'Tracker: {tracker["url"]}', 8), self.config.loglevel)
|
||||
body += logger.print_line(
|
||||
f"{'Not Reverting' if self.config.dry_run else 'Reverting'} to tracker or Global share limits.",
|
||||
self.config.loglevel,
|
||||
)
|
||||
restore_max_ratio = tracker["max_ratio"]
|
||||
restore_max_seeding_time = tracker["max_seeding_time"]
|
||||
restore_limit_upload_speed = tracker["limit_upload_speed"]
|
||||
if restore_max_ratio is None:
|
||||
restore_max_ratio = -2
|
||||
if restore_max_seeding_time is None:
|
||||
restore_max_seeding_time = -2
|
||||
if restore_limit_upload_speed is None:
|
||||
restore_limit_upload_speed = -1
|
||||
if not self.config.dry_run:
|
||||
torrent.remove_tags(tags=self.nohardlinks_tag)
|
||||
body.extend(
|
||||
self.qbt.set_tags_and_limits(
|
||||
torrent, restore_max_ratio, restore_max_seeding_time, restore_limit_upload_speed, restore=True
|
||||
)
|
||||
)
|
||||
if torrent.state_enum.is_complete and self.nohardlinks[category]["resume_torrent_after_untagging_noHL"]:
|
||||
torrent.resume()
|
||||
attr = {
|
||||
"function": "untag_nohardlinks",
|
||||
"title": "Untagging Previous Torrents that now have hardlinks",
|
||||
"body": "\n".join(body),
|
||||
"torrent_name": torrent.name,
|
||||
"torrent_category": torrent.category,
|
||||
"torrent_tag": self.nohardlinks_tag,
|
||||
"torrent_tracker": tracker["url"],
|
||||
"notifiarr_indexer": tracker["notifiarr"],
|
||||
"torrent_max_ratio": restore_max_ratio,
|
||||
"torrent_max_seeding_time": restore_max_seeding_time,
|
||||
"torrent_limit_upload_speed": restore_limit_upload_speed,
|
||||
}
|
||||
self.config.send_notifications(attr)
|
||||
|
||||
def tag_nohardlinks(self):
|
||||
"""Tag torrents with no hardlinks"""
|
||||
logger.separator("Tagging Torrents with No Hardlinks", space=False, border=False)
|
||||
nohardlinks = self.nohardlinks
|
||||
for category in nohardlinks:
|
||||
torrent_list = self.qbt.get_torrents({"category": category, "status_filter": "completed"})
|
||||
if len(torrent_list) == 0:
|
||||
ex = (
|
||||
"No torrents found in the category ("
|
||||
+ category
|
||||
+ ") defined under nohardlinks attribute in the config. "
|
||||
+ "Please check if this matches with any category in qbittorrent and has 1 or more torrents."
|
||||
)
|
||||
logger.warning(ex)
|
||||
continue
|
||||
for torrent in torrent_list:
|
||||
tracker = self.qbt.get_tags(torrent.trackers)
|
||||
has_nohardlinks = util.nohardlink(
|
||||
torrent["content_path"].replace(self.root_dir, self.remote_dir), self.config.notify
|
||||
)
|
||||
if any(tag in torrent.tags for tag in nohardlinks[category]["exclude_tags"]):
|
||||
# Skip to the next torrent if we find any torrents that are in the exclude tag
|
||||
continue
|
||||
else:
|
||||
# Checks for any hardlinks and not already tagged
|
||||
# Cleans up previously tagged nohardlinks_tag torrents that no longer have hardlinks
|
||||
if has_nohardlinks:
|
||||
tracker = self.qbt.get_tags(torrent.trackers)
|
||||
# Determine min_seeding_time.
|
||||
# If only tracker setting is set, use tracker's min_seeding_time
|
||||
# If only nohardlinks category setting is set, use nohardlinks category's min_seeding_time
|
||||
# If both tracker and nohardlinks category setting is set, use the larger of the two
|
||||
# If neither set, use 0 (no limit)
|
||||
min_seeding_time = 0
|
||||
logger.trace(f'tracker["min_seeding_time"] is {tracker["min_seeding_time"]}')
|
||||
logger.trace(f'nohardlinks[category]["min_seeding_time"] is {nohardlinks[category]["min_seeding_time"]}')
|
||||
if tracker["min_seeding_time"] is not None and nohardlinks[category]["min_seeding_time"] is not None:
|
||||
if tracker["min_seeding_time"] >= nohardlinks[category]["min_seeding_time"]:
|
||||
min_seeding_time = tracker["min_seeding_time"]
|
||||
logger.trace(f'Using tracker["min_seeding_time"] {min_seeding_time}')
|
||||
else:
|
||||
min_seeding_time = nohardlinks[category]["min_seeding_time"]
|
||||
logger.trace(f'Using nohardlinks[category]["min_seeding_time"] {min_seeding_time}')
|
||||
elif nohardlinks[category]["min_seeding_time"]:
|
||||
min_seeding_time = nohardlinks[category]["min_seeding_time"]
|
||||
logger.trace(f'Using nohardlinks[category]["min_seeding_time"] {min_seeding_time}')
|
||||
elif tracker["min_seeding_time"]:
|
||||
min_seeding_time = tracker["min_seeding_time"]
|
||||
logger.trace(f'Using tracker["min_seeding_time"] {min_seeding_time}')
|
||||
else:
|
||||
logger.trace(f"Using default min_seeding_time {min_seeding_time}")
|
||||
# Determine max_ratio.
|
||||
# If only tracker setting is set, use tracker's max_ratio
|
||||
# If only nohardlinks category setting is set, use nohardlinks category's max_ratio
|
||||
# If both tracker and nohardlinks category setting is set, use the larger of the two
|
||||
# If neither set, use -1 (no limit)
|
||||
max_ratio = -1
|
||||
logger.trace(f'tracker["max_ratio"] is {tracker["max_ratio"]}')
|
||||
logger.trace(f'nohardlinks[category]["max_ratio"] is {nohardlinks[category]["max_ratio"]}')
|
||||
if tracker["max_ratio"] is not None and nohardlinks[category]["max_ratio"] is not None:
|
||||
if tracker["max_ratio"] >= nohardlinks[category]["max_ratio"]:
|
||||
max_ratio = tracker["max_ratio"]
|
||||
logger.trace(f'Using (tracker["max_ratio"]) {max_ratio}')
|
||||
else:
|
||||
max_ratio = nohardlinks[category]["max_ratio"]
|
||||
logger.trace(f'Using (nohardlinks[category]["max_ratio"]) {max_ratio}')
|
||||
elif nohardlinks[category]["max_ratio"]:
|
||||
max_ratio = nohardlinks[category]["max_ratio"]
|
||||
logger.trace(f'Using (nohardlinks[category]["max_ratio"]) {max_ratio}')
|
||||
elif tracker["max_ratio"]:
|
||||
max_ratio = tracker["max_ratio"]
|
||||
logger.trace(f'Using (tracker["max_ratio"]) {max_ratio}')
|
||||
else:
|
||||
logger.trace(f"Using default (max_ratio) {max_ratio}")
|
||||
# Determine max_seeding_time.
|
||||
# If only tracker setting is set, use tracker's max_seeding_time
|
||||
# If only nohardlinks category setting is set, use nohardlinks category's max_seeding_time
|
||||
# If both tracker and nohardlinks category setting is set, use the larger of the two
|
||||
# If neither set, use -1 (no limit)
|
||||
max_seeding_time = -1
|
||||
logger.trace(f'tracker["max_seeding_time"] is {tracker["max_seeding_time"]}')
|
||||
logger.trace(f'nohardlinks[category]["max_seeding_time"] is {nohardlinks[category]["max_seeding_time"]}')
|
||||
if tracker["max_seeding_time"] is not None and nohardlinks[category]["max_seeding_time"] is not None:
|
||||
if tracker["max_seeding_time"] >= nohardlinks[category]["max_seeding_time"]:
|
||||
max_seeding_time = tracker["max_seeding_time"]
|
||||
logger.trace(f'Using (tracker["max_seeding_time"]) {max_seeding_time}')
|
||||
else:
|
||||
max_seeding_time = nohardlinks[category]["max_seeding_time"]
|
||||
logger.trace(f'Using (nohardlinks[category]["max_seeding_time"]) {max_seeding_time}')
|
||||
elif nohardlinks[category]["max_seeding_time"]:
|
||||
max_seeding_time = nohardlinks[category]["max_seeding_time"]
|
||||
logger.trace(f'Using (nohardlinks[category]["max_seeding_time"]) {max_seeding_time}')
|
||||
elif tracker["max_seeding_time"]:
|
||||
max_seeding_time = tracker["max_seeding_time"]
|
||||
logger.trace(f'Using (tracker["max_seeding_time"]) {max_seeding_time}')
|
||||
else:
|
||||
logger.trace(f"Using default (max_seeding_time) {max_seeding_time}")
|
||||
# Will only tag new torrents that don't have nohardlinks_tag tag
|
||||
if self.nohardlinks_tag not in torrent.tags:
|
||||
self.add_tag_no_hl(
|
||||
torrent=torrent,
|
||||
tracker=tracker,
|
||||
category=category,
|
||||
max_ratio=max_ratio,
|
||||
max_seeding_time=max_seeding_time,
|
||||
add_tag=True,
|
||||
)
|
||||
|
||||
# Deletes torrent with data if cleanup is set to true and meets the ratio/seeding requirements
|
||||
if nohardlinks[category]["cleanup"] and len(nohardlinks[category]) > 0:
|
||||
tor_reach_seed_limit = self.qbt.has_reached_seed_limit(
|
||||
torrent,
|
||||
max_ratio,
|
||||
max_seeding_time,
|
||||
min_seeding_time,
|
||||
nohardlinks[category]["resume_torrent_after_untagging_noHL"],
|
||||
tracker["url"],
|
||||
)
|
||||
if tor_reach_seed_limit:
|
||||
if torrent.hash not in self.tdel_dict:
|
||||
self.tdel_dict[torrent.hash] = {}
|
||||
self.tdel_dict[torrent.hash]["content_path"] = torrent["content_path"].replace(
|
||||
self.root_dir, self.remote_dir
|
||||
)
|
||||
self.tdel_dict[torrent.hash]["body"] = tor_reach_seed_limit
|
||||
else:
|
||||
# Updates torrent to see if "MinSeedTimeNotReached" tag has been added
|
||||
torrent = self.qbt.get_torrents({"torrent_hashes": [torrent.hash]}).data[0]
|
||||
# Checks to see if previously nohardlinks_tag share limits have changed.
|
||||
self.add_tag_no_hl(
|
||||
torrent=torrent,
|
||||
tracker=tracker,
|
||||
category=category,
|
||||
max_ratio=max_ratio,
|
||||
max_seeding_time=max_seeding_time,
|
||||
add_tag=False,
|
||||
)
|
||||
self.check_previous_nohardlinks_tagged_torrents(has_nohardlinks, torrent, tracker, category)
|
||||
self.cleanup_tagged_torrents_with_no_hardlinks(category)
|
||||
if self.stats_tagged >= 1:
|
||||
logger.print_line(
|
||||
f"{'Did not Tag/set' if self.config.dry_run else 'Tag/set'} share limits for {self.stats_tagged} "
|
||||
f".torrent{'s.' if self.stats_tagged > 1 else '.'}",
|
||||
self.config.loglevel,
|
||||
)
|
||||
else:
|
||||
logger.print_line("No torrents to tag with no hardlinks.", self.config.loglevel)
|
||||
if self.stats_untagged >= 1:
|
||||
logger.print_line(
|
||||
f"{'Did not delete' if self.config.dry_run else 'Deleted'} "
|
||||
f"{self.nohardlinks_tag} tags / share limits for {self.stats_untagged} "
|
||||
f".torrent{'s.' if self.stats_untagged > 1 else '.'}",
|
||||
self.config.loglevel,
|
||||
)
|
||||
if self.stats_deleted >= 1:
|
||||
logger.print_line(
|
||||
f"{'Did not delete' if self.config.dry_run else 'Deleted'} {self.stats_deleted} "
|
||||
f".torrent{'s' if self.stats_deleted > 1 else ''} but not content files.",
|
||||
self.config.loglevel,
|
||||
)
|
||||
if self.stats_deleted_contents >= 1:
|
||||
logger.print_line(
|
||||
f"{'Did not delete' if self.config.dry_run else 'Deleted'} {self.stats_deleted_contents} "
|
||||
f".torrent{'s' if self.stats_deleted_contents > 1 else ''} AND content files.",
|
||||
self.config.loglevel,
|
||||
)
|
||||
61
modules/core/tags.py
Normal file
61
modules/core/tags.py
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
from modules import util
|
||||
|
||||
logger = util.logger
|
||||
|
||||
|
||||
class Tags:
|
||||
def __init__(self, qbit_manager):
|
||||
self.qbt = qbit_manager
|
||||
self.config = qbit_manager.config
|
||||
self.client = qbit_manager.client
|
||||
self.stats = 0
|
||||
|
||||
self.tags()
|
||||
|
||||
def tags(self):
|
||||
"""Update tags for torrents"""
|
||||
ignore_tags = self.config.settings["ignoreTags_OnUpdate"]
|
||||
logger.separator("Updating Tags", space=False, border=False)
|
||||
for torrent in self.qbt.torrent_list:
|
||||
check_tags = util.get_list(torrent.tags)
|
||||
if torrent.tags == "" or (len([trk for trk in check_tags if trk not in ignore_tags]) == 0):
|
||||
tracker = self.qbt.get_tags(torrent.trackers)
|
||||
if tracker["tag"]:
|
||||
self.stats += len(tracker["tag"])
|
||||
body = []
|
||||
body += logger.print_line(logger.insert_space(f"Torrent Name: {torrent.name}", 3), self.config.loglevel)
|
||||
body += logger.print_line(
|
||||
logger.insert_space(f'New Tag{"s" if len(tracker["tag"]) > 1 else ""}: {", ".join(tracker["tag"])}', 8),
|
||||
self.config.loglevel,
|
||||
)
|
||||
body += logger.print_line(logger.insert_space(f'Tracker: {tracker["url"]}', 8), self.config.loglevel)
|
||||
body.extend(
|
||||
self.qbt.set_tags_and_limits(
|
||||
torrent,
|
||||
tracker["max_ratio"],
|
||||
tracker["max_seeding_time"],
|
||||
tracker["limit_upload_speed"],
|
||||
tracker["tag"],
|
||||
)
|
||||
)
|
||||
category = self.qbt.get_category(torrent.save_path) if torrent.category == "" else torrent.category
|
||||
attr = {
|
||||
"function": "tag_update",
|
||||
"title": "Updating Tags",
|
||||
"body": "\n".join(body),
|
||||
"torrent_name": torrent.name,
|
||||
"torrent_category": category,
|
||||
"torrent_tag": ", ".join(tracker["tag"]),
|
||||
"torrent_tracker": tracker["url"],
|
||||
"notifiarr_indexer": tracker["notifiarr"],
|
||||
"torrent_max_ratio": tracker["max_ratio"],
|
||||
"torrent_max_seeding_time": tracker["max_seeding_time"],
|
||||
"torrent_limit_upload_speed": tracker["limit_upload_speed"],
|
||||
}
|
||||
self.config.send_notifications(attr)
|
||||
if self.stats >= 1:
|
||||
logger.print_line(
|
||||
f"{'Did not update' if self.config.dry_run else 'Updated'} {self.stats} new tags.", self.config.loglevel
|
||||
)
|
||||
else:
|
||||
logger.print_line("No new torrents to tag.", self.config.loglevel)
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -33,6 +33,44 @@ def get_list(data, lower=False, split=True, int_list=False):
|
|||
return [d.strip() for d in str(data).split(",")]
|
||||
|
||||
|
||||
class TorrentMessages:
|
||||
"""Contains list of messages to check against a status of a torrent"""
|
||||
|
||||
UNREGISTERED_MSGS = [
|
||||
"UNREGISTERED",
|
||||
"TORRENT NOT FOUND",
|
||||
"TORRENT IS NOT FOUND",
|
||||
"NOT REGISTERED",
|
||||
"NOT EXIST",
|
||||
"UNKNOWN TORRENT",
|
||||
"TRUMP",
|
||||
"RETITLED",
|
||||
"TRUNCATED",
|
||||
"TORRENT IS NOT AUTHORIZED FOR USE ON THIS TRACKER",
|
||||
]
|
||||
|
||||
IGNORE_MSGS = [
|
||||
"YOU HAVE REACHED THE CLIENT LIMIT FOR THIS TORRENT",
|
||||
"MISSING PASSKEY",
|
||||
"MISSING INFO_HASH",
|
||||
"PASSKEY IS INVALID",
|
||||
"INVALID PASSKEY",
|
||||
"EXPECTED VALUE (LIST, DICT, INT OR STRING) IN BENCODED STRING",
|
||||
"COULD NOT PARSE BENCODED DATA",
|
||||
"STREAM TRUNCATED",
|
||||
]
|
||||
|
||||
EXCEPTIONS_MSGS = [
|
||||
"DOWN",
|
||||
"DOWN.",
|
||||
"IT MAY BE DOWN,",
|
||||
"UNREACHABLE",
|
||||
"(UNREACHABLE)",
|
||||
"BAD GATEWAY",
|
||||
"TRACKER UNAVAILABLE",
|
||||
]
|
||||
|
||||
|
||||
class check:
|
||||
"""Check for attributes in config."""
|
||||
|
||||
|
|
|
|||
|
|
@ -287,6 +287,13 @@ util.logger = logger
|
|||
from modules.config import Config # noqa
|
||||
from modules.util import GracefulKiller # noqa
|
||||
from modules.util import Failed # noqa
|
||||
from modules.core.category import Category # noqa
|
||||
from modules.core.tags import Tags # noqa
|
||||
from modules.core.remove_unregistered import RemoveUnregistered # noqa
|
||||
from modules.core.cross_seed import CrossSeed # noqa
|
||||
from modules.core.recheck import ReCheck # noqa
|
||||
from modules.core.tag_nohardlinks import TagNoHardLinks # noqa
|
||||
from modules.core.remove_orphaned import RemoveOrphaned # noqa
|
||||
|
||||
|
||||
def my_except_hook(exctype, value, tbi):
|
||||
|
|
@ -369,6 +376,8 @@ def start():
|
|||
|
||||
try:
|
||||
cfg = Config(default_dir, args)
|
||||
qbit_manager = cfg.qbt
|
||||
|
||||
except Exception as ex:
|
||||
if "Qbittorrent Error" in ex.args[0]:
|
||||
logger.print_line(ex, "CRITICAL")
|
||||
|
|
@ -379,53 +388,55 @@ def start():
|
|||
logger.stacktrace()
|
||||
logger.print_line(ex, "CRITICAL")
|
||||
|
||||
if cfg:
|
||||
if qbit_manager:
|
||||
# Set Category
|
||||
num_categorized = cfg.qbt.category()
|
||||
stats["categorized"] += num_categorized
|
||||
if cfg.commands["cat_update"]:
|
||||
stats["categorized"] += Category(qbit_manager).stats
|
||||
|
||||
# Set Tags
|
||||
num_tagged = cfg.qbt.tags()
|
||||
stats["tagged"] += num_tagged
|
||||
if cfg.commands["tag_update"]:
|
||||
stats["tagged"] += Tags(qbit_manager).stats
|
||||
|
||||
# Remove Unregistered Torrents
|
||||
num_deleted, num_deleted_contents, num_tagged, num_untagged = cfg.qbt.rem_unregistered()
|
||||
stats["rem_unreg"] += num_deleted + num_deleted_contents
|
||||
stats["deleted"] += num_deleted
|
||||
stats["deleted_contents"] += num_deleted_contents
|
||||
stats["tagged_tracker_error"] += num_tagged
|
||||
stats["untagged_tracker_error"] += num_untagged
|
||||
stats["tagged"] += num_tagged
|
||||
# Remove Unregistered Torrents and tag errors
|
||||
if cfg.commands["rem_unregistered"] or cfg.commands["tag_tracker_error"]:
|
||||
rem_unreg = RemoveUnregistered(qbit_manager)
|
||||
stats["rem_unreg"] += rem_unreg.stats_deleted + rem_unreg.stats_deleted_contents
|
||||
stats["deleted"] += rem_unreg.stats_deleted
|
||||
stats["deleted_contents"] += rem_unreg.stats_deleted_contents
|
||||
stats["tagged_tracker_error"] += rem_unreg.stats_tagged
|
||||
stats["untagged_tracker_error"] += rem_unreg.stats_untagged
|
||||
stats["tagged"] += rem_unreg.stats_tagged
|
||||
|
||||
# Set Cross Seed
|
||||
num_added, num_tagged = cfg.qbt.cross_seed()
|
||||
stats["added"] += num_added
|
||||
stats["tagged"] += num_tagged
|
||||
if cfg.commands["cross_seed"]:
|
||||
cross_seed = CrossSeed(qbit_manager)
|
||||
stats["added"] += cross_seed.stats_added
|
||||
stats["tagged"] += cross_seed.stats_tagged
|
||||
|
||||
# Recheck Torrents
|
||||
num_resumed, num_rechecked = cfg.qbt.recheck()
|
||||
stats["resumed"] += num_resumed
|
||||
stats["rechecked"] += num_rechecked
|
||||
if cfg.commands["recheck"]:
|
||||
recheck = ReCheck(qbit_manager)
|
||||
stats["resumed"] += recheck.stats_resumed
|
||||
stats["rechecked"] += recheck.stats_rechecked
|
||||
|
||||
# Tag NoHardLinks
|
||||
num_tagged, num_untagged, num_deleted, num_deleted_contents = cfg.qbt.tag_nohardlinks()
|
||||
stats["tagged"] += num_tagged
|
||||
stats["tagged_noHL"] += num_tagged
|
||||
stats["untagged_noHL"] += num_untagged
|
||||
stats["deleted"] += num_deleted
|
||||
stats["deleted_contents"] += num_deleted_contents
|
||||
if cfg.commands["tag_nohardlinks"]:
|
||||
no_hardlinks = TagNoHardLinks(qbit_manager)
|
||||
stats["tagged"] += no_hardlinks.stats_tagged
|
||||
stats["tagged_noHL"] += no_hardlinks.stats_tagged
|
||||
stats["untagged_noHL"] += no_hardlinks.stats_untagged
|
||||
stats["deleted"] += no_hardlinks.stats_deleted
|
||||
stats["deleted_contents"] += no_hardlinks.stats_deleted_contents
|
||||
|
||||
# Remove Orphaned Files
|
||||
num_orphaned = cfg.qbt.rem_orphaned()
|
||||
stats["orphaned"] += num_orphaned
|
||||
if cfg.commands["rem_orphaned"]:
|
||||
stats["orphaned"] += RemoveOrphaned(qbit_manager).stats
|
||||
|
||||
# Empty RecycleBin
|
||||
recycle_emptied = cfg.cleanup_dirs("Recycle Bin")
|
||||
stats["recycle_emptied"] += recycle_emptied
|
||||
stats["recycle_emptied"] += cfg.cleanup_dirs("Recycle Bin")
|
||||
|
||||
# Empty Orphaned Directory
|
||||
orphaned_emptied = cfg.cleanup_dirs("Orphaned Data")
|
||||
stats["orphaned_emptied"] += orphaned_emptied
|
||||
stats["orphaned_emptied"] += cfg.cleanup_dirs("Orphaned Data")
|
||||
|
||||
if stats["categorized"] > 0:
|
||||
stats_summary.append(f"Total Torrents Categorized: {stats['categorized']}")
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
flake8==6.0.0
|
||||
pre-commit==3.2.2
|
||||
qbittorrent-api==2023.3.44
|
||||
qbittorrent-api==2023.4.45
|
||||
requests==2.28.2
|
||||
retrying==1.3.4
|
||||
ruamel.yaml==0.17.21
|
||||
schedule==1.1.0
|
||||
schedule==1.2.0
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue