mirror of
https://github.com/StuffAnThings/qbit_manage.git
synced 2025-11-09 16:00:53 +08:00
commit
30c3beae2d
17 changed files with 1493 additions and 1298 deletions
12
CHANGELOG
12
CHANGELOG
|
|
@ -1,6 +1,8 @@
|
||||||
# Bug Fixes
|
# Requirements Updated
|
||||||
- Fixes #255
|
- Updates qbitorrent api to 2023.4.45
|
||||||
- Fixes #260
|
- Updates Schedule to 1.2.0
|
||||||
- Fixes #258
|
|
||||||
|
|
||||||
**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/releases)
|
||||||
[](https://github.com/StuffAnThings/qbit_manage/tree/develop)
|
[](https://github.com/StuffAnThings/qbit_manage/tree/develop)
|
||||||
[](https://hub.docker.com/r/bobokun/qbit_manage)
|
[](https://hub.docker.com/r/bobokun/qbit_manage)
|
||||||

|

|
||||||
[](https://results.pre-commit.ci/latest/github/StuffAnThings/qbit_manage/master)
|
[](https://results.pre-commit.ci/latest/github/StuffAnThings/qbit_manage/master)
|
||||||
[](https://hub.docker.com/r/bobokun/qbit_manage)
|
[](https://hub.docker.com/r/bobokun/qbit_manage)
|
||||||
[](https://github.com/sponsors/bobokun)
|
[](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")
|
self.notify(e, "Config")
|
||||||
raise Failed(e)
|
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
|
# Empty old files from recycle bin or orphaned
|
||||||
def cleanup_dirs(self, location):
|
def cleanup_dirs(self, location):
|
||||||
num_del = 0
|
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(",")]
|
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:
|
class check:
|
||||||
"""Check for attributes in config."""
|
"""Check for attributes in config."""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -287,6 +287,13 @@ util.logger = logger
|
||||||
from modules.config import Config # noqa
|
from modules.config import Config # noqa
|
||||||
from modules.util import GracefulKiller # noqa
|
from modules.util import GracefulKiller # noqa
|
||||||
from modules.util import Failed # 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):
|
def my_except_hook(exctype, value, tbi):
|
||||||
|
|
@ -369,6 +376,8 @@ def start():
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cfg = Config(default_dir, args)
|
cfg = Config(default_dir, args)
|
||||||
|
qbit_manager = cfg.qbt
|
||||||
|
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
if "Qbittorrent Error" in ex.args[0]:
|
if "Qbittorrent Error" in ex.args[0]:
|
||||||
logger.print_line(ex, "CRITICAL")
|
logger.print_line(ex, "CRITICAL")
|
||||||
|
|
@ -379,53 +388,55 @@ def start():
|
||||||
logger.stacktrace()
|
logger.stacktrace()
|
||||||
logger.print_line(ex, "CRITICAL")
|
logger.print_line(ex, "CRITICAL")
|
||||||
|
|
||||||
if cfg:
|
if qbit_manager:
|
||||||
# Set Category
|
# Set Category
|
||||||
num_categorized = cfg.qbt.category()
|
if cfg.commands["cat_update"]:
|
||||||
stats["categorized"] += num_categorized
|
stats["categorized"] += Category(qbit_manager).stats
|
||||||
|
|
||||||
# Set Tags
|
# Set Tags
|
||||||
num_tagged = cfg.qbt.tags()
|
if cfg.commands["tag_update"]:
|
||||||
stats["tagged"] += num_tagged
|
stats["tagged"] += Tags(qbit_manager).stats
|
||||||
|
|
||||||
# Remove Unregistered Torrents
|
# Remove Unregistered Torrents and tag errors
|
||||||
num_deleted, num_deleted_contents, num_tagged, num_untagged = cfg.qbt.rem_unregistered()
|
if cfg.commands["rem_unregistered"] or cfg.commands["tag_tracker_error"]:
|
||||||
stats["rem_unreg"] += num_deleted + num_deleted_contents
|
rem_unreg = RemoveUnregistered(qbit_manager)
|
||||||
stats["deleted"] += num_deleted
|
stats["rem_unreg"] += rem_unreg.stats_deleted + rem_unreg.stats_deleted_contents
|
||||||
stats["deleted_contents"] += num_deleted_contents
|
stats["deleted"] += rem_unreg.stats_deleted
|
||||||
stats["tagged_tracker_error"] += num_tagged
|
stats["deleted_contents"] += rem_unreg.stats_deleted_contents
|
||||||
stats["untagged_tracker_error"] += num_untagged
|
stats["tagged_tracker_error"] += rem_unreg.stats_tagged
|
||||||
stats["tagged"] += num_tagged
|
stats["untagged_tracker_error"] += rem_unreg.stats_untagged
|
||||||
|
stats["tagged"] += rem_unreg.stats_tagged
|
||||||
|
|
||||||
# Set Cross Seed
|
# Set Cross Seed
|
||||||
num_added, num_tagged = cfg.qbt.cross_seed()
|
if cfg.commands["cross_seed"]:
|
||||||
stats["added"] += num_added
|
cross_seed = CrossSeed(qbit_manager)
|
||||||
stats["tagged"] += num_tagged
|
stats["added"] += cross_seed.stats_added
|
||||||
|
stats["tagged"] += cross_seed.stats_tagged
|
||||||
|
|
||||||
# Recheck Torrents
|
# Recheck Torrents
|
||||||
num_resumed, num_rechecked = cfg.qbt.recheck()
|
if cfg.commands["recheck"]:
|
||||||
stats["resumed"] += num_resumed
|
recheck = ReCheck(qbit_manager)
|
||||||
stats["rechecked"] += num_rechecked
|
stats["resumed"] += recheck.stats_resumed
|
||||||
|
stats["rechecked"] += recheck.stats_rechecked
|
||||||
|
|
||||||
# Tag NoHardLinks
|
# Tag NoHardLinks
|
||||||
num_tagged, num_untagged, num_deleted, num_deleted_contents = cfg.qbt.tag_nohardlinks()
|
if cfg.commands["tag_nohardlinks"]:
|
||||||
stats["tagged"] += num_tagged
|
no_hardlinks = TagNoHardLinks(qbit_manager)
|
||||||
stats["tagged_noHL"] += num_tagged
|
stats["tagged"] += no_hardlinks.stats_tagged
|
||||||
stats["untagged_noHL"] += num_untagged
|
stats["tagged_noHL"] += no_hardlinks.stats_tagged
|
||||||
stats["deleted"] += num_deleted
|
stats["untagged_noHL"] += no_hardlinks.stats_untagged
|
||||||
stats["deleted_contents"] += num_deleted_contents
|
stats["deleted"] += no_hardlinks.stats_deleted
|
||||||
|
stats["deleted_contents"] += no_hardlinks.stats_deleted_contents
|
||||||
|
|
||||||
# Remove Orphaned Files
|
# Remove Orphaned Files
|
||||||
num_orphaned = cfg.qbt.rem_orphaned()
|
if cfg.commands["rem_orphaned"]:
|
||||||
stats["orphaned"] += num_orphaned
|
stats["orphaned"] += RemoveOrphaned(qbit_manager).stats
|
||||||
|
|
||||||
# Empty RecycleBin
|
# Empty RecycleBin
|
||||||
recycle_emptied = cfg.cleanup_dirs("Recycle Bin")
|
stats["recycle_emptied"] += cfg.cleanup_dirs("Recycle Bin")
|
||||||
stats["recycle_emptied"] += recycle_emptied
|
|
||||||
|
|
||||||
# Empty Orphaned Directory
|
# Empty Orphaned Directory
|
||||||
orphaned_emptied = cfg.cleanup_dirs("Orphaned Data")
|
stats["orphaned_emptied"] += cfg.cleanup_dirs("Orphaned Data")
|
||||||
stats["orphaned_emptied"] += orphaned_emptied
|
|
||||||
|
|
||||||
if stats["categorized"] > 0:
|
if stats["categorized"] > 0:
|
||||||
stats_summary.append(f"Total Torrents Categorized: {stats['categorized']}")
|
stats_summary.append(f"Total Torrents Categorized: {stats['categorized']}")
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
flake8==6.0.0
|
flake8==6.0.0
|
||||||
pre-commit==3.2.2
|
pre-commit==3.2.2
|
||||||
qbittorrent-api==2023.3.44
|
qbittorrent-api==2023.4.45
|
||||||
requests==2.28.2
|
requests==2.28.2
|
||||||
retrying==1.3.4
|
retrying==1.3.4
|
||||||
ruamel.yaml==0.17.21
|
ruamel.yaml==0.17.21
|
||||||
schedule==1.1.0
|
schedule==1.2.0
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue