Skip to content

Commit

Permalink
Merge pull request #336 from StuffAnThings/develop
Browse files Browse the repository at this point in the history
4.0.1
  • Loading branch information
bobokun authored Jun 16, 2023
2 parents a36a28b + a42db58 commit 1d262de
Show file tree
Hide file tree
Showing 22 changed files with 228 additions and 123 deletions.
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ repos:
hooks:
- id: reorder-python-imports
- repo: https://github.com/asottile/pyupgrade
rev: v3.4.0
rev: v3.6.0
hooks:
- id: pyupgrade
args: [--py3-plus]
Expand All @@ -45,7 +45,7 @@ repos:
hooks:
- id: black
language_version: python3
args: [--line-length, '130']
args: [--line-length, '130', --preview]
- repo: https://github.com/PyCQA/flake8
rev: 6.0.0
hooks:
Expand Down
41 changes: 22 additions & 19 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
# Requirements Updated
- Updates ruamel.yaml to 0.17.31
- Updates qbitorrent-api to 2023.5.48
- Separate out dev requirements into requirements-dev.txt
# Breaking Changes
- `tag_nohardlinks` only updates/removes `noHL` tag. **It does not modify or cleanup share_limits anymore.**
- `tag_update` only adds tracker tags to torrent. **It does not modify or cleanup share_limits anymore.**
- Please remove any references to share_limits from your configuration in the tracker/nohardlinks section
- Migration guide can be followed here: [V4 Migration Guide](https://github.com/StuffAnThings/qbit_manage/wiki/v4-Migration-Guide)
- Webhook payloads changed (See [webhooks](https://github.com/StuffAnThings/qbit_manage/wiki/Config-Setup#webhooks) for updated payload)

- qbitorrent-api updated to 2023.6.49
# New Features
- Adds new command `share_limits`, `--share-limits` , `QBT_SHARE_LIMITS=True` to update share limits based on tags/categories specified per group (Closes #88, Closes #306, Closes #259, Closes #308, Closes #137)
- See [Config Setup - share_limits](https://github.com/StuffAnThings/qbit_manage/wiki/Config-Setup#share_limits) for more details
- Adds new command `skip_qb_version_check`, `--skip-qb-version-check`, `QBT_SKIP_QB_VERSION_CHECK` to bypass qbitorrent compatibility check (unsupported - Thanks to @ftc2 #307)
- Updates to webhook notifications to group notifications when a function updates more than 10 Torrents.
- Adds new webhooks for `share_limits`
- Adds rate limit to webhook notifications (1 msg/sec)
- cross-seed will move torrent to an error folder if it fails to inject torrent
- Define multiple announce urls for one tracker (#328 Thanks to @buthed010203 for the PR)

# Bug Fixes
- Fixes #302
- Fixes #317
- Fixes #329 (Updates missing share_limits tag even when share_limits are satisfied)
- Fixes #327 (Zero value share limits not being applied correctly)

# Enhancements
- Logic for `share_limits_suffix_tag` changed to become a prefix tag instead along with adding the priority of the group. The reason for this change is so it's easier to see the share limit groups togethered in qbitorrent ordered by priority.
- `share_limits_suffix_tag` key is now `share_limits_tag`
- No config changes are required as the qbm will automatically change the previous `share_limits_suffix_tag` key to `share_limits_tag`
- Changes the default value of `share_limits_tag` to `~share_limit`. The `~` is used to sort the `share_limits_tag` in the qbt webUI to the bottom for less clutter

Example based on config.sample:

| old tag (v4.0.0) | new tag (v4.0.1) |
| ----------- | ----------- |
| noHL.share_limit | ~share_limit_1.noHL |
| cross-seed.share_limit | ~share_limit_2.cross-seed |
| PTP.share_limit | ~share_limit_3.PTP |
| default.share_limit | ~share_limit_999.default |

**Full Changelog**: https://github.com/StuffAnThings/qbit_manage/compare/v3.6.4...v4.0.0
**Full Changelog**: https://github.com/StuffAnThings/qbit_manage/compare/v4.0.0...v4.0.1
10 changes: 9 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,19 @@ venv: requirements.txt setup.py tox.ini

.PHONY: test
test:
tox
tox -e tests

.PHONY: pre-commit
pre-commit:
tox -e pre-commit

.PHONY: clean
clean:
find -name '*.pyc' -delete
find -name '__pycache__' -delete
rm -rf .tox
rm -rf venv

.PHONY: install-hooks
install-hooks:
tox -e install-hooks
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@

This is a program used to manage your qBittorrent instance such as:

* Tag torrents based on tracker URL and set seed goals/limit upload speed by tag (only tag torrents that have no tags)
* Tag torrents based on tracker URLs
* Update categories based on save directory
* Remove unregistered torrents (delete data & torrent if it is not being cross-seeded, otherwise it will just remove the torrent)
* Automatically add [cross-seed](https://github.com/mmgoodnow/cross-seed) torrents in paused state. **\*Note: cross-seed now allows for torrent injections directly to qBit, making this feature obsolete.\***
* Recheck paused torrents sorted by lowest size and resume if completed
* Remove orphaned files from your root directory that are not referenced by qBittorrent
* Tag any torrents that have no hard links outisde the root folder and allows optional cleanup to delete these torrents and contents based on maximum ratio and/or time seeded
* Tag any torrents that have no hard links outisde the root folder
* Apply share limits based on groups filtered by tags/categories and allows optional cleanup to delete these torrents and contents based on maximum ratio and/or time seeded
* RecycleBin function to move files into a RecycleBin folder instead of deleting the data directly when deleting a torrent
* Built-in scheduler to run the script every x minutes. (Can use `--run` command to run without the scheduler)
* Webhook notifications with [Notifiarr](https://notifiarr.com/) and [Apprise API](https://github.com/caronc/apprise-api) integration
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
4.0.0
4.0.1
9 changes: 3 additions & 6 deletions config/config.yml.sample
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ settings:
force_auto_tmm: False # Will force qBittorrent to enable Automatic Torrent Management for each torrent.
tracker_error_tag: issue # Will set the tag of any torrents that do not have a working tracker.
nohardlinks_tag: noHL # Will set the tag of any torrents with no hardlinks.
share_limits_suffix_tag: share_limit # Will add this suffix to the grouping separated by '.' to the tag of any torrents with share limits.
share_limits_tag: ~share_limit # Will add this tag when applying share limits to provide an easy way to filter torrents by share limit group/priority for each torrent
ignoreTags_OnUpdate: # When running tag-update function, it will update torrent tags for a given torrent even if the torrent has at least one or more of the tags defined here. Otherwise torrents will not be tagged if tags exist.
- noHL
- issue
Expand Down Expand Up @@ -67,7 +67,7 @@ cat_change:
tracker:
# Mandatory
# Tag Parameters
# <Tracker URL Keyword>: # <MANDATORY> This is the keyword in the tracker url
# <Tracker URL Keyword>: # <MANDATORY> This is the keyword in the tracker url. You can define multiple tracker urls by splitting with `|` delimiter
# <MANDATORY> Set tag name. Can be a list of tags or a single tag
# tag: <Tag Name>
# <OPTIONAL> Set this to the notifiarr react name. This is used to add indexer reactions to the notifications sent by Notifiarr
Expand Down Expand Up @@ -107,13 +107,10 @@ tracker:
privatehd:
tag: PrivateHD
notifiarr:
tleechreload:
tag: TorrentLeech
notifiarr: torrentleech
torrentdb:
tag: TorrentDB
notifiarr: torrentdb
torrentleech:
torrentleech|tleechreload:
tag: TorrentLeech
notifiarr: torrentleech
tv-vault:
Expand Down
51 changes: 36 additions & 15 deletions modules/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,12 @@ def __init__(self, default_dir, args):
if "cat_change" in self.data:
self.data["cat_change"] = self.data.pop("cat_change")
if "tracker" in self.data:
self.data["tracker"] = self.data.pop("tracker")
trackers = self.data.pop("tracker")
self.data["tracker"] = {}
# Splits tracker urls at pipes, useful for trackers with multiple announce urls
for tracker_urls, data in trackers.items():
for tracker_url in tracker_urls.split("|"):
self.data["tracker"][tracker_url.strip()] = data
else:
self.data["tracker"] = {}
if "nohardlinks" in self.data:
Expand Down Expand Up @@ -146,6 +151,11 @@ def hooks(attr):
self.loglevel = "DRYRUN" if self.dry_run else "INFO"
self.session = requests.Session()

share_limits_tag = self.data["settings"].get("share_limits_suffix_tag", "~share_limit")
# Convert previous share_limits_suffix_tag to new default share_limits_tag
if share_limits_tag == "share_limit":
share_limits_tag = "~share_limit"

self.settings = {
"force_auto_tmm": self.util.check_for_attribute(
self.data, "force_auto_tmm", parent="settings", var_type="bool", default=False
Expand All @@ -154,19 +164,22 @@ def hooks(attr):
self.data, "tracker_error_tag", parent="settings", default="issue"
),
"nohardlinks_tag": self.util.check_for_attribute(self.data, "nohardlinks_tag", parent="settings", default="noHL"),
"share_limits_suffix_tag": self.util.check_for_attribute(
self.data, "share_limits_suffix_tag", parent="settings", default="share_limit"
"share_limits_tag": self.util.check_for_attribute(
self.data, "share_limits_tag", parent="settings", default=share_limits_tag
),
}

self.tracker_error_tag = self.settings["tracker_error_tag"]
self.nohardlinks_tag = self.settings["nohardlinks_tag"]
self.share_limits_suffix_tag = "." + self.settings["share_limits_suffix_tag"]
self.share_limits_tag = self.settings["share_limits_tag"]

default_ignore_tags = [self.nohardlinks_tag, self.tracker_error_tag, "cross-seed"]
self.settings["ignoreTags_OnUpdate"] = self.util.check_for_attribute(
self.data, "ignoreTags_OnUpdate", parent="settings", default=default_ignore_tags, var_type="list"
)
"Migrate settings from v4.0.0 to v4.0.1 and beyond. Convert 'share_limits_suffix_tag' to 'share_limits_tag'"
if "share_limits_suffix_tag" in self.data["settings"]:
self.util.overwrite_attributes(self.settings, "settings")

default_function = {
"cross_seed": None,
Expand Down Expand Up @@ -303,7 +316,7 @@ def _sort_share_limits(share_limits):
else:
priority = max(priorities) + 1
logger.warning(
f"Priority not defined for the grouping '{key}' in share_limits. " f"Setting priority to {priority}"
f"Priority not defined for the grouping '{key}' in share_limits. Setting priority to {priority}"
)
value["priority"] = self.util.check_for_attribute(
self.data,
Expand Down Expand Up @@ -538,14 +551,18 @@ def _sort_share_limits(share_limits):
)
if self.commands["rem_orphaned"]:
exclude_orphaned = f"**{os.sep}{os.path.basename(self.orphaned_dir.rstrip(os.sep))}{os.sep}*"
self.orphaned["exclude_patterns"].append(exclude_orphaned) if exclude_orphaned not in self.orphaned[
"exclude_patterns"
] else self.orphaned["exclude_patterns"]
(
self.orphaned["exclude_patterns"].append(exclude_orphaned)
if exclude_orphaned not in self.orphaned["exclude_patterns"]
else self.orphaned["exclude_patterns"]
)
if self.recyclebin["enabled"]:
exclude_recycle = f"**{os.sep}{os.path.basename(self.recycle_dir.rstrip(os.sep))}{os.sep}*"
self.orphaned["exclude_patterns"].append(exclude_recycle) if exclude_recycle not in self.orphaned[
"exclude_patterns"
] else self.orphaned["exclude_patterns"]
(
self.orphaned["exclude_patterns"].append(exclude_recycle)
if exclude_recycle not in self.orphaned["exclude_patterns"]
else self.orphaned["exclude_patterns"]
)

# Connect to Qbittorrent
self.qbt = None
Expand Down Expand Up @@ -635,8 +652,10 @@ def cleanup_dirs(self, location):
if empty_after_x_days <= days:
num_del += 1
body += logger.print_line(
f"{'Did not delete' if self.dry_run else 'Deleted'} "
f"{filename} from {folder} (Last modified {round(days)} days ago).",
(
f"{'Did not delete' if self.dry_run else 'Deleted'} "
f"{filename} from {folder} (Last modified {round(days)} days ago)."
),
self.loglevel,
)
files += [str(filename)]
Expand All @@ -649,8 +668,10 @@ def cleanup_dirs(self, location):
for path in location_path_list:
util.remove_empty_directories(path, "**/*")
body += logger.print_line(
f"{'Did not delete' if self.dry_run else 'Deleted'} {num_del} files "
f"({util.human_readable_size(size_bytes)}) from the {location}.",
(
f"{'Did not delete' if self.dry_run else 'Deleted'} {num_del} files "
f"({util.human_readable_size(size_bytes)}) from the {location}."
),
self.loglevel,
)
attr = {
Expand Down
8 changes: 6 additions & 2 deletions modules/core/cross_seed.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,21 @@ def cross_seed(self):
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)
dir_cs_err = os.path.join(dir_cs, "qbit_manage_error")
os.makedirs(dir_cs_err, 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()))
src = os.path.join(dir_cs, file)
dir_cs_out = os.path.join(dir_cs_out, file)
dir_cs_err = os.path.join(dir_cs_err, file)
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.torrentinfo[t_name].get("Category", self.qbt.get_category(dest))
# Only add cross-seed torrent if original torrent is complete
if self.qbt.torrentinfo[t_name]["is_complete"]:
Expand Down Expand Up @@ -98,6 +101,7 @@ def cross_seed(self):
logger.print_line(error, self.config.loglevel)
else:
logger.print_line(error, "WARNING")
util.move_files(src, dir_cs_err)
self.config.notify(error, "cross-seed", False)

self.config.webhooks_factory.notify(self.torrents_updated, self.notify_attr, group_by="category")
Expand Down
8 changes: 5 additions & 3 deletions modules/core/recheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,10 @@ def recheck(self):
)
logger.debug(
logger.insert_space(
f"-- Seeding Time vs Max Seed Time: {timedelta(seconds=torrent.seeding_time)} < "
f"{timedelta(minutes=torrent.max_seeding_time)}",
(
f"-- Seeding Time vs Max Seed Time: {timedelta(seconds=torrent.seeding_time)} < "
f"{timedelta(minutes=torrent.max_seeding_time)}"
),
4,
)
)
Expand All @@ -85,7 +87,7 @@ def recheck(self):
):
self.stats_resumed += 1
body = logger.print_line(
f"{'Not Resuming' if self.config.dry_run else 'Resuming'} [{tracker['tag']}] - " f"{t_name}",
f"{'Not Resuming' if self.config.dry_run else 'Resuming'} [{tracker['tag']}] - {t_name}",
self.config.loglevel,
)
attr = {
Expand Down
6 changes: 4 additions & 2 deletions modules/core/remove_orphaned.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,10 @@ def rem_orphaned(self):
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"{'Not moving' if self.config.dry_run else 'Moving'} {num_orphaned} Orphaned files "
f"to {self.orphaned_dir.replace(self.remote_dir,self.root_dir)}",
(
f"{'Not moving' if self.config.dry_run else 'Moving'} {num_orphaned} Orphaned files "
f"to {self.orphaned_dir.replace(self.remote_dir,self.root_dir)}"
),
self.config.loglevel,
)

Expand Down
18 changes: 12 additions & 6 deletions modules/core/remove_unregistered.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,22 +145,28 @@ def rem_unregistered(self):
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.",
(
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.",
(
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 '.'}",
(
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:
Expand Down
Loading

0 comments on commit 1d262de

Please sign in to comment.