Merge pull request #264 from StuffAnThings/develop

3.6.0
This commit is contained in:
bobokun 2023-04-12 14:48:23 -04:00 committed by GitHub
commit 30c3beae2d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 1493 additions and 1298 deletions

View file

@ -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

View file

@ -3,7 +3,7 @@
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/StuffAnThings/qbit_manage?style=plastic)](https://github.com/StuffAnThings/qbit_manage/releases)
[![GitHub commits since latest release (by SemVer)](https://img.shields.io/github/commits-since/StuffAnThings/qbit_manage/latest/develop?label=Commits%20in%20Develop&style=plastic)](https://github.com/StuffAnThings/qbit_manage/tree/develop)
[![Docker Image Version (latest semver)](https://img.shields.io/docker/v/bobokun/qbit_manage?label=docker&sort=semver&style=plastic)](https://hub.docker.com/r/bobokun/qbit_manage)
![Github Workflow Status](https://img.shields.io/github/workflow/status/StuffAnThings/qbit_manage/Docker%20Latest%20Release?style=plastic)
![Github Workflow Status](https://img.shields.io/github/actions/workflow/status/StuffAnThings/qbit_manage/latest.yml?style=plastic)
[![pre-commit.ci status](https://results.pre-commit.ci/badge/github/StuffAnThings/qbit_manage/master.svg)](https://results.pre-commit.ci/latest/github/StuffAnThings/qbit_manage/master)
[![Docker Pulls](https://img.shields.io/docker/pulls/bobokun/qbit_manage?style=plastic)](https://hub.docker.com/r/bobokun/qbit_manage)
[![Sponsor or Donate](https://img.shields.io/badge/-Sponsor_or_Donate-blueviolet?style=plastic)](https://github.com/sponsors/bobokun)

View file

@ -1 +1 @@
3.5.1
3.6.0

20
modules/__init__.py Normal file
View 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__)

View file

@ -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
View 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
View 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
View 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
View 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()

View 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)

View 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

View 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
View 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

View file

@ -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."""

View file

@ -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']}")

View file

@ -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