Merge pull request #499 from StuffAnThings/develop

4.0.9
This commit is contained in:
bobokun 2024-02-27 14:45:29 -08:00 committed by GitHub
commit 29690e9581
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 334 additions and 133 deletions

View file

@ -12,6 +12,11 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: set lower case owner name
run: |
echo "OWNER_LC=${OWNER,,}" >>${GITHUB_ENV}
env:
OWNER: '${{ github.repository_owner }}'
- name: Check Out Repo - name: Check Out Repo
uses: actions/checkout@v4 uses: actions/checkout@v4
@ -24,6 +29,13 @@ jobs:
username: ${{ secrets.DOCKER_HUB_USERNAME }} username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Login to ghcr.io
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ env.OWNER_LC }}
password: ${{ secrets.GHCR_TOKEN }}
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@master uses: docker/setup-qemu-action@master
with: with:
@ -43,7 +55,9 @@ jobs:
"BRANCH_NAME=develop" "BRANCH_NAME=develop"
platforms: linux/amd64,linux/arm64,linux/arm/v7 platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true push: true
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/qbit_manage:develop tags: |
${{ secrets.DOCKER_HUB_USERNAME }}/qbit_manage:develop
ghcr.io/${{ env.OWNER_LC }}/qbit_manage:develop
- name: Trigger Hotio Webhook - name: Trigger Hotio Webhook
uses: joelwmale/webhook-action@master uses: joelwmale/webhook-action@master

View file

@ -10,23 +10,28 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: set lower case owner name
run: |
echo "OWNER_LC=${OWNER,,}" >>${GITHUB_ENV}
env:
OWNER: '${{ github.repository_owner }}'
- name: Check Out Repo - name: Check Out Repo
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Trigger Hotio Webhook
uses: joelwmale/webhook-action@master
with:
url: ${{ secrets.HOTIO_WEBHOOK_URL }}
headers: '{"Authorization": "Bearer ${{ secrets.HOTIO_WEBHOOK_SECRET }}"}'
body: '{ "application": "qbitmanage", "branch": "release" }'
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
username: ${{ secrets.DOCKER_HUB_USERNAME }} username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Login to ghcr.io
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ env.OWNER_LC }}
password: ${{ secrets.GHCR_TOKEN }}
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@master uses: docker/setup-qemu-action@master
with: with:
@ -44,4 +49,13 @@ jobs:
file: ./Dockerfile file: ./Dockerfile
platforms: linux/amd64,linux/arm64,linux/arm/v7 platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true push: true
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/qbit_manage:latest tags: |
${{ secrets.DOCKER_HUB_USERNAME }}/qbit_manage:latest
ghcr.io/${{ env.OWNER_LC }}/qbit_manage:latest
- name: Trigger Hotio Webhook
uses: joelwmale/webhook-action@master
with:
url: ${{ secrets.HOTIO_WEBHOOK_URL }}
headers: '{"Authorization": "Bearer ${{ secrets.HOTIO_WEBHOOK_SECRET }}"}'
body: '{ "application": "qbitmanage", "branch": "" }'

View file

@ -11,6 +11,11 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: set lower case owner name
run: |
echo "OWNER_LC=${OWNER,,}" >>${GITHUB_ENV}
env:
OWNER: '${{ github.repository_owner }}'
- name: Check Out Repo - name: Check Out Repo
uses: actions/checkout@v4 uses: actions/checkout@v4
@ -23,6 +28,13 @@ jobs:
username: ${{ secrets.DOCKER_HUB_USERNAME }} username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Login to ghcr.io
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ env.OWNER_LC }}
password: ${{ secrets.GHCR_TOKEN }}
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@master uses: docker/setup-qemu-action@master
with: with:
@ -44,8 +56,9 @@ jobs:
file: ./Dockerfile file: ./Dockerfile
platforms: linux/amd64,linux/arm64,linux/arm/v7 platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true push: true
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/qbit_manage:${{ steps.get_version.outputs.VERSION }} tags: |
${{ secrets.DOCKER_HUB_USERNAME }}/qbit_manage:${{ steps.get_version.outputs.VERSION }}
ghcr.io/${{ env.OWNER_LC }}/qbit_manage:${{ steps.get_version.outputs.VERSION }}
- name: Create release - name: Create release
id: create_release id: create_release
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1

View file

@ -21,7 +21,7 @@ repos:
hooks: hooks:
- id: autopep8 - id: autopep8
- repo: https://github.com/adrienverge/yamllint.git - repo: https://github.com/adrienverge/yamllint.git
rev: v1.33.0 # or higher tag rev: v1.35.1 # or higher tag
hooks: hooks:
- id: yamllint - id: yamllint
args: [--format, parsable, --strict] args: [--format, parsable, --strict]
@ -31,17 +31,19 @@ repos:
hooks: hooks:
- id: yamlfix - id: yamlfix
exclude: ^.github/ exclude: ^.github/
- repo: https://github.com/asottile/reorder-python-imports - repo: https://github.com/pycqa/isort
rev: v3.12.0 rev: 5.13.2
hooks: hooks:
- id: reorder-python-imports - id: isort
name: isort (python)
args: [--force-single-line-imports, --profile, black]
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v3.15.0 rev: v3.15.1
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: [--py3-plus] args: [--py3-plus]
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 23.12.1 rev: 24.2.0
hooks: hooks:
- id: black - id: black
language_version: python3 language_version: python3

View file

@ -1,8 +1,22 @@
# Requirements Updated # Requirements Updated
- qbittorrent-api==2024.1.58 - qbittorrent-api==2024.2.59
- GitPython==3.1.42
- ruamel.yaml==0.18.6
# Updates # New Features
- Adds arguments for mover script (Adds #473) - Adds support for filtering more than just Completed torrents. Closes [#115](https://github.com/StuffAnThings/qbit_manage/issues/115)
- Updates mover script (Add check if file is still on cache mount #493)
- Adds support for ghcr.io container registry
- Adds support for custom [share_limits/cross-seed tags](https://github.com/StuffAnThings/qbit_manage/commit/9f8be69a4f2680501d492a8c7148969ae5ac5b72#diff-e5794b6d2186004aa3ee69cd4dee7bbd48d8e0edd9f1da90d03393ec28cbf912) Closes [#457](https://github.com/StuffAnThings/qbit_manage/issues/457)
Special thanks to @NooNameR for their contributions!
**Full Changelog**: https://github.com/StuffAnThings/qbit_manage/compare/v4.0.7...v4.0.8 # Bug Fixes
- Fixes [#359](https://github.com/StuffAnThings/qbit_manage/issues/359)
- Fixes [#479](https://github.com/StuffAnThings/qbit_manage/issues/479)
- Fixes [#487](https://github.com/StuffAnThings/qbit_manage/issues/487)
- Fixes [#488](https://github.com/StuffAnThings/qbit_manage/issues/488)
- Fixes [#490](https://github.com/StuffAnThings/qbit_manage/issues/490)
- Update script header so that env python3 is used.
Special thanks to @NooNameR, @ShanaryS, @ext4xfs for their contributions!
**Full Changelog**: https://github.com/StuffAnThings/qbit_manage/compare/v4.0.8...v4.0.9

View file

@ -1 +1 @@
4.0.8 4.0.9

View file

@ -29,10 +29,20 @@ settings:
tracker_error_tag: issue # Will set the tag of any torrents that do not have a working tracker. 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. nohardlinks_tag: noHL # Will set the tag of any torrents with no hardlinks.
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 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
share_limits_min_seeding_time_tag: MinSeedTimeNotReached # Tag to be added to torrents that have not yet reached the minimum seeding time
share_limits_min_num_seeds_tag: MinSeedsNotMet # Tag to be added to torrents that have not yet reached the minimum number of seeds
share_limits_last_active_tag: LastActiveLimitNotReached # Tag to be added to torrents that have not yet reached the last active limit
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. 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 - noHL
- issue - issue
- cross-seed - cross-seed
- MinSeedTimeNotReached
- MinSeedsNotMet
- LastActiveLimitNotReached
cross_seed_tag: cross-seed # Will set the tag of any torrents that are added by cross-seed command
cat_filter_completed: True # Filters for completed torrents only when running cat_update command
share_limits_filter_completed: True # Filters for completed torrents only when running share_limits command
tag_nohardlinks_filter_completed: True # Filters for completed torrents only when running tag_nohardlinks command
directory: directory:
# Do not remove these # Do not remove these
# Cross-seed var: </your/path/here/> # Output directory of cross-seed # Cross-seed var: </your/path/here/> # Output directory of cross-seed
@ -172,10 +182,10 @@ share_limits:
categories: categories:
- RadarrComplete - RadarrComplete
- SonarrComplete - SonarrComplete
# <OPTIONAL> max_ratio <float>: Will set the torrent Maximum share ratio until torrent is stopped from seeding/uploading. # <OPTIONAL> max_ratio <float>: Will set the torrent Maximum share ratio until torrent is stopped from seeding/uploading and may be cleaned up / removed if the minimums have been met.
# Will default to -1 (no limit) if not specified for the group. # Will default to -1 (no limit) if not specified for the group.
max_ratio: 5.0 max_ratio: 5.0
# <OPTIONAL> max_seeding_time <int>: Will set the torrent Maximum seeding time (minutes) until torrent is stopped from seeding. # <OPTIONAL> max_seeding_time <int>: Will set the torrent Maximum seeding time (minutes) until torrent is stopped from seeding/uploading and may be cleaned up / removed if the minimums have been met.
# Will default to -1 (no limit) if not specified for the group. # Will default to -1 (no limit) if not specified for the group.
max_seeding_time: 129600 max_seeding_time: 129600
# <OPTIONAL> min_seeding_time <int>: Will prevent torrent deletion by cleanup variable if torrent has not yet minimum seeding time (minutes). # <OPTIONAL> min_seeding_time <int>: Will prevent torrent deletion by cleanup variable if torrent has not yet minimum seeding time (minutes).
@ -188,14 +198,14 @@ share_limits:
last_active: 43200 last_active: 43200
# <OPTIONAL> Limit Upload Speed <int>: Will limit the upload speed KiB/s (KiloBytes/second) (`-1` : No Limit) # <OPTIONAL> Limit Upload Speed <int>: Will limit the upload speed KiB/s (KiloBytes/second) (`-1` : No Limit)
limit_upload_speed: 0 limit_upload_speed: 0
# <OPTIONAL> cleanup <bool>: WARNING!! Setting this as true Will remove and delete contents of any torrents that satisfies the share limits # <OPTIONAL> cleanup <bool>: WARNING!! Setting this as true Will remove and delete contents of any torrents that satisfies the share limits (max time OR max ratio)
cleanup: false cleanup: false
# <OPTIONAL> resume_torrent_after_change <bool>: This variable will resume your torrent after changing share limits. Default is true # <OPTIONAL> resume_torrent_after_change <bool>: This variable will resume your torrent after changing share limits. Default is true
resume_torrent_after_change: true resume_torrent_after_change: true
# <OPTIONAL> add_group_to_tag <bool>: This adds your grouping as a tag with a prefix defined in settings . Default is true # <OPTIONAL> add_group_to_tag <bool>: This adds your grouping as a tag with a prefix defined in settings . Default is true
# Example: A grouping defined as noHL will have a tag set to ~share_limit.noHL (if using the default prefix) # Example: A grouping defined as noHL will have a tag set to ~share_limit.noHL (if using the default prefix)
add_group_to_tag: true add_group_to_tag: true
# <OPTIONAL> min_num_seeds <int>: This will prevent torrent deletion by cleanup variable if the number of seeds is less than the value set here. # <OPTIONAL> min_num_seeds <int>: Will prevent torrent deletion by cleanup variable if the number of seeds is less than the value set here.
# If the torrent has less number of seeds than the min_num_seeds, the share limits will be changed back to no limits and resume the torrent to continue seeding. # If the torrent has less number of seeds than the min_num_seeds, the share limits will be changed back to no limits and resume the torrent to continue seeding.
# Will default to 0 if not specified for the group. # Will default to 0 if not specified for the group.
min_num_seeds: 0 min_num_seeds: 0

View file

@ -1,4 +1,5 @@
"""Apprise notification class""" """Apprise notification class"""
import time import time
from modules import util from modules import util

View file

@ -1,4 +1,5 @@
"""Module for BeyondHD (BHD) tracker.""" """Module for BeyondHD (BHD) tracker."""
from json import JSONDecodeError from json import JSONDecodeError
from modules import util from modules import util

View file

@ -1,4 +1,5 @@
"""Config class for qBittorrent-Manage""" """Config class for qBittorrent-Manage"""
import os import os
import re import re
import stat import stat
@ -13,9 +14,9 @@ from modules.apprise import Apprise
from modules.bhd import BeyondHD from modules.bhd import BeyondHD
from modules.notifiarr import Notifiarr from modules.notifiarr import Notifiarr
from modules.qbittorrent import Qbt from modules.qbittorrent import Qbt
from modules.util import check
from modules.util import Failed
from modules.util import YAML from modules.util import YAML
from modules.util import Failed
from modules.util import check
from modules.webhooks import Webhooks from modules.webhooks import Webhooks
logger = util.logger logger = util.logger
@ -167,17 +168,47 @@ class Config:
"share_limits_tag": self.util.check_for_attribute( "share_limits_tag": self.util.check_for_attribute(
self.data, "share_limits_tag", parent="settings", default=share_limits_tag self.data, "share_limits_tag", parent="settings", default=share_limits_tag
), ),
"share_limits_min_seeding_time_tag": self.util.check_for_attribute(
self.data, "share_limits_min_seeding_time_tag", parent="settings", default="MinSeedTimeNotReached"
),
"share_limits_min_num_seeds_tag": self.util.check_for_attribute(
self.data, "share_limits_min_num_seeds_tag", parent="settings", default="MinSeedsNotMet"
),
"share_limits_last_active_tag": self.util.check_for_attribute(
self.data, "share_limits_last_active_tag", parent="settings", default="LastActiveLimitNotReached"
),
"cross_seed_tag": self.util.check_for_attribute(self.data, "cross_seed_tag", parent="settings", default="cross-seed"),
"cat_filter_completed": self.util.check_for_attribute(
self.data, "cat_filter_completed", parent="settings", var_type="bool", default=True
),
"share_limits_filter_completed": self.util.check_for_attribute(
self.data, "share_limits_filter_completed", parent="settings", var_type="bool", default=True
),
"tag_nohardlinks_filter_completed": self.util.check_for_attribute(
self.data, "tag_nohardlinks_filter_completed", parent="settings", var_type="bool", default=True
),
} }
self.tracker_error_tag = self.settings["tracker_error_tag"] self.tracker_error_tag = self.settings["tracker_error_tag"]
self.nohardlinks_tag = self.settings["nohardlinks_tag"] self.nohardlinks_tag = self.settings["nohardlinks_tag"]
self.share_limits_tag = self.settings["share_limits_tag"] self.share_limits_tag = self.settings["share_limits_tag"]
self.share_limits_min_seeding_time_tag = self.settings["share_limits_min_seeding_time_tag"]
self.share_limits_min_num_seeds_tag = self.settings["share_limits_min_num_seeds_tag"]
self.share_limits_last_active_tag = self.settings["share_limits_last_active_tag"]
self.cross_seed_tag = self.settings["cross_seed_tag"]
default_ignore_tags = [self.nohardlinks_tag, self.tracker_error_tag, "cross-seed"] self.default_ignore_tags = [
self.nohardlinks_tag,
self.tracker_error_tag,
self.cross_seed_tag,
self.share_limits_min_seeding_time_tag,
self.share_limits_min_num_seeds_tag,
self.share_limits_last_active_tag,
]
self.settings["ignoreTags_OnUpdate"] = self.util.check_for_attribute( self.settings["ignoreTags_OnUpdate"] = self.util.check_for_attribute(
self.data, "ignoreTags_OnUpdate", parent="settings", default=default_ignore_tags, var_type="list" self.data, "ignoreTags_OnUpdate", parent="settings", default=self.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'" # "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"]: if "share_limits_suffix_tag" in self.data["settings"]:
self.util.overwrite_attributes(self.settings, "settings") self.util.overwrite_attributes(self.settings, "settings")
@ -280,6 +311,8 @@ class Config:
cat_str = list(cat.keys())[0] cat_str = list(cat.keys())[0]
self.nohardlinks[cat_str] = {} self.nohardlinks[cat_str] = {}
exclude_tags = cat[cat_str].get("exclude_tags", []) exclude_tags = cat[cat_str].get("exclude_tags", [])
if exclude_tags is None:
exclude_tags = []
if isinstance(exclude_tags, str): if isinstance(exclude_tags, str):
exclude_tags = [exclude_tags] exclude_tags = [exclude_tags]
self.nohardlinks[cat_str]["exclude_tags"] = exclude_tags self.nohardlinks[cat_str]["exclude_tags"] = exclude_tags
@ -687,6 +720,7 @@ class Config:
if num_del > 0: if num_del > 0:
if not self.dry_run: if not self.dry_run:
for path in location_path_list: for path in location_path_list:
if path != location_path:
util.remove_empty_directories(path, "**/*") util.remove_empty_directories(path, "**/*")
body += logger.print_line( body += logger.print_line(
f"{'Did not delete' if self.dry_run else 'Deleted'} {num_del} files " f"{'Did not delete' if self.dry_run else 'Deleted'} {num_del} files "

View file

@ -14,6 +14,7 @@ class Category:
self.torrents_updated = [] # List of torrents updated self.torrents_updated = [] # List of torrents updated
self.notify_attr = [] # List of single torrent attributes to send to notifiarr self.notify_attr = [] # List of single torrent attributes to send to notifiarr
self.uncategorized_mapping = "Uncategorized" self.uncategorized_mapping = "Uncategorized"
self.status_filter = "completed" if self.config.settings["cat_filter_completed"] else "all"
self.category() self.category()
self.config.webhooks_factory.notify(self.torrents_updated, self.notify_attr, group_by="category") self.config.webhooks_factory.notify(self.torrents_updated, self.notify_attr, group_by="category")
@ -21,7 +22,7 @@ class Category:
def category(self): def category(self):
"""Update category for torrents that don't have any category defined and returns total number categories updated""" """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) logger.separator("Updating Categories", space=False, border=False)
torrent_list = self.qbt.get_torrents({"category": "", "status_filter": "completed"}) torrent_list = self.qbt.get_torrents({"category": "", "status_filter": self.status_filter})
for torrent in torrent_list: for torrent in torrent_list:
new_cat = self.get_tracker_cat(torrent) or self.qbt.get_category(torrent.save_path) new_cat = self.get_tracker_cat(torrent) or self.qbt.get_category(torrent.save_path)
if new_cat == self.uncategorized_mapping: if new_cat == self.uncategorized_mapping:
@ -32,7 +33,7 @@ class Category:
# Change categories # Change categories
if self.config.cat_change: if self.config.cat_change:
for old_cat in self.config.cat_change: for old_cat in self.config.cat_change:
torrent_list = self.qbt.get_torrents({"category": old_cat, "status_filter": "completed"}) torrent_list = self.qbt.get_torrents({"category": old_cat, "status_filter": self.status_filter})
for torrent in torrent_list: for torrent in torrent_list:
new_cat = self.config.cat_change[old_cat] new_cat = self.config.cat_change[old_cat]
self.update_cat(torrent, new_cat, True) self.update_cat(torrent, new_cat, True)

View file

@ -14,6 +14,7 @@ class CrossSeed:
self.client = qbit_manager.client self.client = qbit_manager.client
self.stats_added = 0 self.stats_added = 0
self.stats_tagged = 0 self.stats_tagged = 0
self.cross_seed_tag = qbit_manager.config.cross_seed_tag
self.torrents_updated = [] # List of torrents added by cross-seed self.torrents_updated = [] # List of torrents added by cross-seed
self.notify_attr = [] # List of single torrent attributes to send to notifiarr self.notify_attr = [] # List of single torrent attributes to send to notifiarr
@ -40,8 +41,8 @@ class CrossSeed:
# Returned the dictionary of filtered item # Returned the dictionary of filtered item
torrentdict_file = dict(filter(lambda item: tr_name in item[0], self.qbt.torrentinfo.items())) torrentdict_file = dict(filter(lambda item: tr_name in item[0], self.qbt.torrentinfo.items()))
src = os.path.join(dir_cs, file) src = os.path.join(dir_cs, file)
dir_cs_out = os.path.join(dir_cs_out, file) file_cs_out = os.path.join(dir_cs_out, file)
dir_cs_err = os.path.join(dir_cs_err, file) file_cs_err = os.path.join(dir_cs_err, file)
if torrentdict_file: if torrentdict_file:
# Get the exact torrent match name from self.qbt.torrentinfo # Get the exact torrent match name from self.qbt.torrentinfo
t_name = next(iter(torrentdict_file)) t_name = next(iter(torrentdict_file))
@ -65,7 +66,7 @@ class CrossSeed:
"torrents": [t_name], "torrents": [t_name],
"torrent_category": category, "torrent_category": category,
"torrent_save_path": dest, "torrent_save_path": dest,
"torrent_tag": "cross-seed", "torrent_tag": self.cross_seed_tag,
"torrent_tracker": t_tracker, "torrent_tracker": t_tracker,
} }
self.notify_attr.append(attr) self.notify_attr.append(attr)
@ -73,13 +74,12 @@ class CrossSeed:
self.stats_added += 1 self.stats_added += 1
if not self.config.dry_run: if not self.config.dry_run:
self.client.torrents.add( self.client.torrents.add(
torrent_files=src, save_path=dest, category=category, tags="cross-seed", is_paused=True torrent_files=src, save_path=dest, category=category, tags=self.cross_seed_tag, is_paused=True
) )
self.qbt.torrentinfo[t_name]["count"] += 1
try: try:
torrent_hash_generator = TorrentHashGenerator(src) torrent_hash_generator = TorrentHashGenerator(src)
torrent_hash = torrent_hash_generator.generate_torrent_hash() torrent_hash = torrent_hash_generator.generate_torrent_hash()
util.move_files(src, dir_cs_out) util.move_files(src, file_cs_out)
except Exception as e: except Exception as e:
logger.warning(f"Unable to generate torrent hash from cross-seed {t_name}: {e}") logger.warning(f"Unable to generate torrent hash from cross-seed {t_name}: {e}")
try: try:
@ -89,6 +89,7 @@ class CrossSeed:
logger.warning(f"Unable to find hash {torrent_hash} in qbt: {e}") logger.warning(f"Unable to find hash {torrent_hash} in qbt: {e}")
if torrent_info: if torrent_info:
torrent = torrent_info[0] torrent = torrent_info[0]
self.qbt.add_torrent_files(torrent.hash, torrent.files)
self.qbt.torrentvalid.append(torrent) self.qbt.torrentvalid.append(torrent)
self.qbt.torrentinfo[t_name]["torrents"].append(torrent) self.qbt.torrentinfo[t_name]["torrents"].append(torrent)
self.qbt.torrent_list.append(torrent) self.qbt.torrent_list.append(torrent)
@ -101,7 +102,7 @@ class CrossSeed:
logger.print_line(error, self.config.loglevel) logger.print_line(error, self.config.loglevel)
else: else:
logger.print_line(error, "WARNING") logger.print_line(error, "WARNING")
util.move_files(src, dir_cs_err) util.move_files(src, file_cs_err)
self.config.notify(error, "cross-seed", False) self.config.notify(error, "cross-seed", False)
self.config.webhooks_factory.notify(self.torrents_updated, self.notify_attr, group_by="category") self.config.webhooks_factory.notify(self.torrents_updated, self.notify_attr, group_by="category")
@ -112,14 +113,16 @@ class CrossSeed:
t_name = torrent.name t_name = torrent.name
t_cat = torrent.category t_cat = torrent.category
if ( if (
not util.is_tag_in_torrent("cross-seed", torrent.tags) not util.is_tag_in_torrent(self.cross_seed_tag, torrent.tags)
and self.qbt.torrentinfo[t_name]["count"] > 1 and self.qbt.is_cross_seed(torrent)
and self.qbt.torrentinfo[t_name]["first_hash"] != torrent.hash and torrent.downloaded == 0
and torrent.seeding_time > 0
): ):
tracker = self.qbt.get_tags(torrent.trackers) tracker = self.qbt.get_tags(torrent.trackers)
self.stats_tagged += 1 self.stats_tagged += 1
body = logger.print_line( body = logger.print_line(
f"{'Not Adding' if self.config.dry_run else 'Adding'} 'cross-seed' tag to {t_name}", self.config.loglevel f"{'Not Adding' if self.config.dry_run else 'Adding'} '{self.cross_seed_tag}' tag to {t_name}",
self.config.loglevel,
) )
attr = { attr = {
"function": "tag_cross_seed", "function": "tag_cross_seed",
@ -127,13 +130,13 @@ class CrossSeed:
"body": body, "body": body,
"torrents": [t_name], "torrents": [t_name],
"torrent_category": t_cat, "torrent_category": t_cat,
"torrent_tag": "cross-seed", "torrent_tag": self.cross_seed_tag,
"torrent_tracker": tracker["url"], "torrent_tracker": tracker["url"],
} }
self.notify_attr.append(attr) self.notify_attr.append(attr)
self.torrents_updated.append(t_name) self.torrents_updated.append(t_name)
if not self.config.dry_run: if not self.config.dry_run:
torrent.add_tags(tags="cross-seed") torrent.add_tags(tags=self.cross_seed_tag)
self.config.webhooks_factory.notify(self.torrents_updated, self.notify_attr, group_by="category") self.config.webhooks_factory.notify(self.torrents_updated, self.notify_attr, group_by="category")
numcategory = Counter(categories) numcategory = Counter(categories)
for cat in numcategory: for cat in numcategory:

View file

@ -2,8 +2,8 @@ from qbittorrentapi import NotFound404Error
from qbittorrentapi import TrackerStatus from qbittorrentapi import TrackerStatus
from modules import util from modules import util
from modules.util import list_in_text
from modules.util import TorrentMessages from modules.util import TorrentMessages
from modules.util import list_in_text
logger = util.logger logger = util.logger
@ -209,7 +209,7 @@ class RemoveUnregistered:
"torrent_tracker": tracker["url"], "torrent_tracker": tracker["url"],
"notifiarr_indexer": tracker["notifiarr"], "notifiarr_indexer": tracker["notifiarr"],
} }
if self.qbt.torrentinfo[self.t_name]["count"] > 1: if self.qbt.has_cross_seed(torrent):
# Checks if any of the original torrents are working # Checks if any of the original torrents are working
if "" in self.t_msg or 2 in self.t_status: if "" in self.t_msg or 2 in self.t_status:
attr["torrents_deleted_and_contents"] = False attr["torrents_deleted_and_contents"] = False
@ -232,4 +232,3 @@ class RemoveUnregistered:
attr["body"] = "\n".join(body) attr["body"] = "\n".join(body)
self.torrents_updated_unreg.append(self.t_name) self.torrents_updated_unreg.append(self.t_name)
self.notify_attr_unreg.append(attr) self.notify_attr_unreg.append(attr)
self.qbt.torrentinfo[self.t_name]["count"] -= 1

View file

@ -8,10 +8,6 @@ from modules.webhooks import GROUP_NOTIFICATION_LIMIT
logger = util.logger logger = util.logger
MIN_SEEDING_TIME_TAG = "MinSeedTimeNotReached"
MIN_NUM_SEEDS_TAG = "MinSeedsNotMet"
LAST_ACTIVE_TAG = "LastActiveLimitNotReached"
class ShareLimits: class ShareLimits:
def __init__(self, qbit_manager): def __init__(self, qbit_manager):
@ -23,6 +19,7 @@ class ShareLimits:
# meets the criteria for ratio limit/seed limit for deletion # meets the criteria for ratio limit/seed limit for deletion
self.stats_deleted_contents = 0 # counter for the number of torrents that \ self.stats_deleted_contents = 0 # counter for the number of torrents that \
# meets the criteria for ratio limit/seed limit for deletion including contents \ # meets the criteria for ratio limit/seed limit for deletion including contents \
self.status_filter = "completed" if self.config.settings["share_limits_filter_completed"] else "all"
self.tdel_dict = {} # dictionary to track the torrent names and content path that meet the deletion criteria 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 # root directory of torrents self.root_dir = qbit_manager.config.root_dir # root directory of torrents
@ -31,6 +28,9 @@ class ShareLimits:
self.torrents_updated = [] # list of torrents that have been updated self.torrents_updated = [] # list of torrents that have been updated
self.torrent_hash_checked = [] # list of torrent hashes that have been checked for share limits self.torrent_hash_checked = [] # list of torrent hashes that have been checked for share limits
self.share_limits_tag = qbit_manager.config.share_limits_tag # tag for share limits self.share_limits_tag = qbit_manager.config.share_limits_tag # tag for share limits
self.min_seeding_time_tag = qbit_manager.config.share_limits_min_seeding_time_tag # tag for min seeding time
self.min_num_seeds_tag = qbit_manager.config.share_limits_min_num_seeds_tag # tag for min num seeds
self.last_active_tag = qbit_manager.config.share_limits_last_active_tag # tag for last active
self.group_tag = None # tag for the share limit group self.group_tag = None # tag for the share limit group
self.update_share_limits() self.update_share_limits()
@ -39,7 +39,7 @@ class ShareLimits:
def update_share_limits(self): def update_share_limits(self):
"""Updates share limits for torrents based on grouping""" """Updates share limits for torrents based on grouping"""
logger.separator("Updating Share Limits based on priority", space=False, border=False) logger.separator("Updating Share Limits based on priority", space=False, border=False)
torrent_list = self.qbt.get_torrents({"status_filter": "completed"}) torrent_list = self.qbt.get_torrents({"status_filter": self.status_filter})
self.assign_torrents_to_group(torrent_list) self.assign_torrents_to_group(torrent_list)
for group_name, group_config in self.share_limits_config.items(): for group_name, group_config in self.share_limits_config.items():
torrents = group_config["torrents"] torrents = group_config["torrents"]
@ -79,7 +79,6 @@ class ShareLimits:
for torrent_hash, torrent_dict in self.tdel_dict.items(): for torrent_hash, torrent_dict in self.tdel_dict.items():
torrent = torrent_dict["torrent"] torrent = torrent_dict["torrent"]
t_name = torrent.name t_name = torrent.name
t_count = self.qbt.torrentinfo[t_name]["count"]
t_msg = self.qbt.torrentinfo[t_name]["msg"] t_msg = self.qbt.torrentinfo[t_name]["msg"]
t_status = self.qbt.torrentinfo[t_name]["status"] t_status = self.qbt.torrentinfo[t_name]["status"]
# Double check that the content path is the same before we delete anything # Double check that the content path is the same before we delete anything
@ -105,7 +104,7 @@ class ShareLimits:
} }
if os.path.exists(torrent["content_path"].replace(self.root_dir, self.remote_dir)): if os.path.exists(torrent["content_path"].replace(self.root_dir, self.remote_dir)):
# Checks if any of the original torrents are working # Checks if any of the original torrents are working
if t_count > 1 and ("" in t_msg or 2 in t_status): if self.qbt.has_cross_seed(torrent) and ("" in t_msg or 2 in t_status):
self.stats_deleted += 1 self.stats_deleted += 1
attr["torrents_deleted_and_contents"] = False attr["torrents_deleted_and_contents"] = False
t_deleted.add(t_name) t_deleted.add(t_name)
@ -136,7 +135,6 @@ class ShareLimits:
attr["body"] = "\n".join(body) attr["body"] = "\n".join(body)
if not group_notifications: if not group_notifications:
self.config.send_notifications(attr) self.config.send_notifications(attr)
self.qbt.torrentinfo[t_name]["count"] -= 1
if group_notifications: if group_notifications:
if t_deleted: if t_deleted:
attr = { attr = {
@ -213,9 +211,9 @@ class ShareLimits:
check_max_ratio or check_max_seeding_time or check_limit_upload_speed or share_limits_not_yet_tagged check_max_ratio or check_max_seeding_time or check_limit_upload_speed or share_limits_not_yet_tagged
) and hash_not_prev_checked: ) and hash_not_prev_checked:
if ( if (
not is_tag_in_torrent(MIN_SEEDING_TIME_TAG, torrent.tags) not is_tag_in_torrent(self.min_seeding_time_tag, torrent.tags)
and not is_tag_in_torrent(MIN_NUM_SEEDS_TAG, torrent.tags) and not is_tag_in_torrent(self.min_num_seeds_tag, torrent.tags)
and not is_tag_in_torrent(LAST_ACTIVE_TAG, torrent.tags) and not is_tag_in_torrent(self.last_active_tag, torrent.tags)
): ):
logger.print_line(logger.insert_space(f"Torrent Name: {t_name}", 3), self.config.loglevel) logger.print_line(logger.insert_space(f"Torrent Name: {t_name}", 3), self.config.loglevel)
logger.print_line(logger.insert_space(f'Tracker: {tracker["url"]}', 8), self.config.loglevel) logger.print_line(logger.insert_space(f'Tracker: {tracker["url"]}', 8), self.config.loglevel)
@ -373,11 +371,11 @@ class ShareLimits:
max_ratio = torrent.max_ratio max_ratio = torrent.max_ratio
if max_seeding_time is None: if max_seeding_time is None:
max_seeding_time = torrent.max_seeding_time max_seeding_time = torrent.max_seeding_time
if is_tag_in_torrent(MIN_SEEDING_TIME_TAG, torrent.tags): if is_tag_in_torrent(self.min_seeding_time_tag, torrent.tags):
return [] return []
if is_tag_in_torrent(MIN_NUM_SEEDS_TAG, torrent.tags): if is_tag_in_torrent(self.min_num_seeds_tag, torrent.tags):
return [] return []
if is_tag_in_torrent(LAST_ACTIVE_TAG, torrent.tags): if is_tag_in_torrent(self.last_active_tag, torrent.tags):
return [] return []
torrent.set_share_limits(ratio_limit=max_ratio, seeding_time_limit=max_seeding_time, inactive_seeding_time_limit=-2) torrent.set_share_limits(ratio_limit=max_ratio, seeding_time_limit=max_seeding_time, inactive_seeding_time_limit=-2)
return body return body
@ -391,12 +389,12 @@ class ShareLimits:
def _has_reached_min_seeding_time_limit(): def _has_reached_min_seeding_time_limit():
print_log = [] print_log = []
if torrent.seeding_time >= min_seeding_time * 60: if torrent.seeding_time >= min_seeding_time * 60:
if is_tag_in_torrent(MIN_SEEDING_TIME_TAG, torrent.tags): if is_tag_in_torrent(self.min_seeding_time_tag, torrent.tags):
if not self.config.dry_run: if not self.config.dry_run:
torrent.remove_tags(tags=MIN_SEEDING_TIME_TAG) torrent.remove_tags(tags=self.min_seeding_time_tag)
return True return True
else: else:
if not is_tag_in_torrent(MIN_SEEDING_TIME_TAG, torrent.tags): if not is_tag_in_torrent(self.min_seeding_time_tag, torrent.tags):
print_log += logger.print_line(logger.insert_space(f"Torrent Name: {torrent.name}", 3), self.config.loglevel) print_log += logger.print_line(logger.insert_space(f"Torrent Name: {torrent.name}", 3), self.config.loglevel)
print_log += logger.print_line(logger.insert_space(f"Tracker: {tracker}", 8), self.config.loglevel) print_log += logger.print_line(logger.insert_space(f"Tracker: {tracker}", 8), self.config.loglevel)
print_log += logger.print_line( print_log += logger.print_line(
@ -409,10 +407,10 @@ class ShareLimits:
self.config.loglevel, self.config.loglevel,
) )
print_log += logger.print_line( print_log += logger.print_line(
logger.insert_space(f"Adding Tag: {MIN_SEEDING_TIME_TAG}", 8), self.config.loglevel logger.insert_space(f"Adding Tag: {self.min_seeding_time_tag}", 8), self.config.loglevel
) )
if not self.config.dry_run: if not self.config.dry_run:
torrent.add_tags(MIN_SEEDING_TIME_TAG) torrent.add_tags(self.min_seeding_time_tag)
torrent.set_share_limits(ratio_limit=-1, seeding_time_limit=-1, inactive_seeding_time_limit=-1) torrent.set_share_limits(ratio_limit=-1, seeding_time_limit=-1, inactive_seeding_time_limit=-1)
if resume_torrent: if resume_torrent:
torrent.resume() torrent.resume()
@ -421,12 +419,12 @@ class ShareLimits:
def _is_less_than_min_num_seeds(): def _is_less_than_min_num_seeds():
print_log = [] print_log = []
if min_num_seeds == 0 or torrent.num_complete >= min_num_seeds: if min_num_seeds == 0 or torrent.num_complete >= min_num_seeds:
if is_tag_in_torrent(MIN_NUM_SEEDS_TAG, torrent.tags): if is_tag_in_torrent(self.min_num_seeds_tag, torrent.tags):
if not self.config.dry_run: if not self.config.dry_run:
torrent.remove_tags(tags=MIN_NUM_SEEDS_TAG) torrent.remove_tags(tags=self.min_num_seeds_tag)
return False return False
else: else:
if not is_tag_in_torrent(MIN_NUM_SEEDS_TAG, torrent.tags): if not is_tag_in_torrent(self.min_num_seeds_tag, torrent.tags):
print_log += logger.print_line(logger.insert_space(f"Torrent Name: {torrent.name}", 3), self.config.loglevel) print_log += logger.print_line(logger.insert_space(f"Torrent Name: {torrent.name}", 3), self.config.loglevel)
print_log += logger.print_line(logger.insert_space(f"Tracker: {tracker}", 8), self.config.loglevel) print_log += logger.print_line(logger.insert_space(f"Tracker: {tracker}", 8), self.config.loglevel)
print_log += logger.print_line( print_log += logger.print_line(
@ -439,10 +437,10 @@ class ShareLimits:
self.config.loglevel, self.config.loglevel,
) )
print_log += logger.print_line( print_log += logger.print_line(
logger.insert_space(f"Adding Tag: {MIN_NUM_SEEDS_TAG}", 8), self.config.loglevel logger.insert_space(f"Adding Tag: {self.min_num_seeds_tag}", 8), self.config.loglevel
) )
if not self.config.dry_run: if not self.config.dry_run:
torrent.add_tags(MIN_NUM_SEEDS_TAG) torrent.add_tags(self.min_num_seeds_tag)
torrent.set_share_limits(ratio_limit=-1, seeding_time_limit=-1, inactive_seeding_time_limit=-1) torrent.set_share_limits(ratio_limit=-1, seeding_time_limit=-1, inactive_seeding_time_limit=-1)
if resume_torrent: if resume_torrent:
torrent.resume() torrent.resume()
@ -453,12 +451,12 @@ class ShareLimits:
now = int(time()) now = int(time())
inactive_time_minutes = round((now - torrent.last_activity) / 60) inactive_time_minutes = round((now - torrent.last_activity) / 60)
if inactive_time_minutes >= last_active: if inactive_time_minutes >= last_active:
if is_tag_in_torrent(LAST_ACTIVE_TAG, torrent.tags): if is_tag_in_torrent(self.last_active_tag, torrent.tags):
if not self.config.dry_run: if not self.config.dry_run:
torrent.remove_tags(tags=LAST_ACTIVE_TAG) torrent.remove_tags(tags=self.last_active_tag)
return True return True
else: else:
if not is_tag_in_torrent(LAST_ACTIVE_TAG, torrent.tags): if not is_tag_in_torrent(self.last_active_tag, torrent.tags):
print_log += logger.print_line(logger.insert_space(f"Torrent Name: {torrent.name}", 3), self.config.loglevel) print_log += logger.print_line(logger.insert_space(f"Torrent Name: {torrent.name}", 3), self.config.loglevel)
print_log += logger.print_line(logger.insert_space(f"Tracker: {tracker}", 8), self.config.loglevel) print_log += logger.print_line(logger.insert_space(f"Tracker: {tracker}", 8), self.config.loglevel)
print_log += logger.print_line( print_log += logger.print_line(
@ -470,9 +468,11 @@ class ShareLimits:
), ),
self.config.loglevel, self.config.loglevel,
) )
print_log += logger.print_line(logger.insert_space(f"Adding Tag: {LAST_ACTIVE_TAG}", 8), self.config.loglevel) print_log += logger.print_line(
logger.insert_space(f"Adding Tag: {self.last_active_tag}", 8), self.config.loglevel
)
if not self.config.dry_run: if not self.config.dry_run:
torrent.add_tags(LAST_ACTIVE_TAG) torrent.add_tags(self.last_active_tag)
torrent.set_share_limits(ratio_limit=-1, seeding_time_limit=-1, inactive_seeding_time_limit=-1) torrent.set_share_limits(ratio_limit=-1, seeding_time_limit=-1, inactive_seeding_time_limit=-1)
if resume_torrent: if resume_torrent:
torrent.resume() torrent.resume()

View file

@ -22,6 +22,8 @@ class TagNoHardLinks:
self.torrents_updated_untagged = [] # List of torrents updated self.torrents_updated_untagged = [] # List of torrents updated
self.notify_attr_untagged = [] # List of single torrent attributes to send to notifiarr self.notify_attr_untagged = [] # List of single torrent attributes to send to notifiarr
self.status_filter = "completed" if self.config.settings["tag_nohardlinks_filter_completed"] else "all"
self.tag_nohardlinks() self.tag_nohardlinks()
self.config.webhooks_factory.notify(self.torrents_updated_tagged, self.notify_attr_tagged, group_by="tag") self.config.webhooks_factory.notify(self.torrents_updated_tagged, self.notify_attr_tagged, group_by="tag")
@ -87,7 +89,7 @@ class TagNoHardLinks:
nohardlinks = self.nohardlinks nohardlinks = self.nohardlinks
check_hardlinks = util.CheckHardLinks(self.root_dir, self.remote_dir) check_hardlinks = util.CheckHardLinks(self.root_dir, self.remote_dir)
for category in nohardlinks: for category in nohardlinks:
torrent_list = self.qbt.get_torrents({"category": category, "status_filter": "completed"}) torrent_list = self.qbt.get_torrents({"category": category, "status_filter": self.status_filter})
if len(torrent_list) == 0: if len(torrent_list) == 0:
ex = ( ex = (
"No torrents found in the category (" "No torrents found in the category ("

View file

@ -10,6 +10,7 @@ class Tags:
self.client = qbit_manager.client self.client = qbit_manager.client
self.stats = 0 self.stats = 0
self.share_limits_tag = qbit_manager.config.share_limits_tag # suffix tag for share limits self.share_limits_tag = qbit_manager.config.share_limits_tag # suffix tag for share limits
self.default_ignore_tags = qbit_manager.config.default_ignore_tags # default ignore tags
self.torrents_updated = [] # List of torrents updated self.torrents_updated = [] # List of torrents updated
self.notify_attr = [] # List of single torrent attributes to send to notifiarr self.notify_attr = [] # List of single torrent attributes to send to notifiarr
@ -19,6 +20,7 @@ class Tags:
def tags(self): def tags(self):
"""Update tags for torrents""" """Update tags for torrents"""
ignore_tags = self.config.settings["ignoreTags_OnUpdate"] ignore_tags = self.config.settings["ignoreTags_OnUpdate"]
ignore_tags.extend(tag for tag in self.default_ignore_tags if tag not in ignore_tags)
logger.separator("Updating Tags", space=False, border=False) logger.separator("Updating Tags", space=False, border=False)
for torrent in self.qbt.torrent_list: for torrent in self.qbt.torrent_list:
check_tags = [tag for tag in util.get_list(torrent.tags) if self.share_limits_tag not in tag] check_tags = [tag for tag in util.get_list(torrent.tags) if self.share_limits_tag not in tag]

View file

@ -1,4 +1,5 @@
"""Logging module""" """Logging module"""
import io import io
import logging import logging
import os import os

View file

@ -1,4 +1,5 @@
"""Qbittorrent Module""" """Qbittorrent Module"""
import os import os
import sys import sys
@ -10,8 +11,8 @@ from qbittorrentapi import Version
from modules import util from modules import util
from modules.util import Failed from modules.util import Failed
from modules.util import list_in_text
from modules.util import TorrentMessages from modules.util import TorrentMessages
from modules.util import list_in_text
logger = util.logger logger = util.logger
@ -70,15 +71,16 @@ class Qbt:
logger.print_line(ex, "CRITICAL") logger.print_line(ex, "CRITICAL")
sys.exit(1) sys.exit(1)
logger.info("Qbt Connection Successful") logger.info("Qbt Connection Successful")
except LoginFailed as exc: except LoginFailed:
ex = "Qbittorrent Error: Failed to login. Invalid username/password." ex = "Qbittorrent Error: Failed to login. Invalid username/password."
self.config.notify(ex, "Qbittorrent") self.config.notify(ex, "Qbittorrent")
raise Failed(exc) from exc raise Failed(ex)
except Exception as exc: except Exception as exc:
self.config.notify(exc, "Qbittorrent") self.config.notify(exc, "Qbittorrent")
raise Failed(exc) from exc raise Failed(exc)
logger.separator("Getting Torrent List", space=False, border=False) logger.separator("Getting Torrent List", space=False, border=False)
self.torrent_list = self.get_torrents({"sort": "added_on"}) self.torrent_list = self.get_torrents({"sort": "added_on"})
self.torrentfiles = {} # a map of torrent files to track cross-seeds
self.global_max_ratio_enabled = self.client.app.preferences.max_ratio_enabled self.global_max_ratio_enabled = self.client.app.preferences.max_ratio_enabled
self.global_max_ratio = self.client.app.preferences.max_ratio self.global_max_ratio = self.client.app.preferences.max_ratio
@ -96,12 +98,11 @@ class Qbt:
def get_torrent_info(self): def get_torrent_info(self):
""" """
Will create a 2D Dictionary with the torrent name as the key Will create a 2D Dictionary with the torrent name as the key
self.torrentinfo = {'TorrentName1' : {'Category':'TV', 'save_path':'/data/torrents/TV', 'count':1, 'msg':'[]'...}, self.torrentinfo = {'TorrentName1' : {'Category':'TV', 'save_path':'/data/torrents/TV', 'msg':'[]'...},
'TorrentName2' : {'Category':'Movies', 'save_path':'/data/torrents/Movies'}, 'count':2, 'msg':'[]'...} 'TorrentName2' : {'Category':'Movies', 'save_path':'/data/torrents/Movies'}, 'msg':'[]'...}
List of dictionary key definitions List of dictionary key definitions
Category = Returns category of the torrent (str) Category = Returns category of the torrent (str)
save_path = Returns the save path of the torrent (str) save_path = Returns the save path of the torrent (str)
count = Returns a count of the total number of torrents with the same name (int)
msg = Returns a list of torrent messages by name (list of str) msg = Returns a list of torrent messages by name (list of str)
status = Returns the list of status numbers of the torrent by name status = Returns the list of status numbers of the torrent by name
(0: Tracker is disabled (used for DHT, PeX, and LSD), (0: Tracker is disabled (used for DHT, PeX, and LSD),
@ -111,8 +112,6 @@ class Qbt:
4: Tracker has been contacted, but it is not working (or doesn't send proper replies) 4: Tracker has been contacted, but it is not working (or doesn't send proper replies)
is_complete = Returns the state of torrent is_complete = Returns the state of torrent
(Returns True if at least one of the torrent with the State is categorized as Complete.) (Returns True if at least one of the torrent with the State is categorized as Complete.)
first_hash = Returns the hash number of the original torrent (Assuming the torrent list is sorted by date added (Asc))
Takes in a number n, returns the square of n
""" """
self.torrentinfo = {} self.torrentinfo = {}
self.torrentissue = [] # list of unregistered torrent objects self.torrentissue = [] # list of unregistered torrent objects
@ -140,23 +139,20 @@ class Qbt:
save_path = torrent.save_path save_path = torrent.save_path
category = torrent.category category = torrent.category
torrent_trackers = torrent.trackers torrent_trackers = torrent.trackers
self.add_torrent_files(torrent_hash, torrent.files)
except Exception as ex: except Exception as ex:
self.config.notify(ex, "Get Torrent Info", False) self.config.notify(ex, "Get Torrent Info", False)
logger.warning(ex) logger.warning(ex)
if torrent_name in self.torrentinfo: if torrent_name in self.torrentinfo:
t_obj_list.append(torrent) t_obj_list.append(torrent)
t_count = self.torrentinfo[torrent_name]["count"] + 1
msg_list = self.torrentinfo[torrent_name]["msg"] msg_list = self.torrentinfo[torrent_name]["msg"]
status_list = self.torrentinfo[torrent_name]["status"] status_list = self.torrentinfo[torrent_name]["status"]
is_complete = True if self.torrentinfo[torrent_name]["is_complete"] is True else torrent_is_complete is_complete = True if self.torrentinfo[torrent_name]["is_complete"] is True else torrent_is_complete
first_hash = self.torrentinfo[torrent_name]["first_hash"]
else: else:
t_obj_list = [torrent] t_obj_list = [torrent]
t_count = 1
msg_list = [] msg_list = []
status_list = [] status_list = []
is_complete = torrent_is_complete is_complete = torrent_is_complete
first_hash = torrent_hash
for trk in torrent_trackers: for trk in torrent_trackers:
if trk.url.startswith("http"): if trk.url.startswith("http"):
status = trk.status status = trk.status
@ -187,14 +183,80 @@ class Qbt:
"torrents": t_obj_list, "torrents": t_obj_list,
"Category": category, "Category": category,
"save_path": save_path, "save_path": save_path,
"count": t_count,
"msg": msg_list, "msg": msg_list,
"status": status_list, "status": status_list,
"is_complete": is_complete, "is_complete": is_complete,
"first_hash": first_hash,
} }
self.torrentinfo[torrent_name] = torrentattr self.torrentinfo[torrent_name] = torrentattr
def add_torrent_files(self, torrent_hash, torrent_files):
"""Process torrent files by adding the hash to the appropriate torrent_files list.
Example structure:
torrent_files = {
"folder1/file1.txt": {"original": torrent_hash1, "cross_seed": ["torrent_hash2", "torrent_hash3"]},
"folder1/file2.txt": {"original": torrent_hash1, "cross_seed": ["torrent_hash2"]},
"folder2/file1.txt": {"original": torrent_hash2, "cross_seed": []},
}
"""
for file in torrent_files:
file_name = file.name
if file_name not in self.torrentfiles:
self.torrentfiles[file_name] = {"original": torrent_hash, "cross_seed": []}
else:
self.torrentfiles[file_name]["cross_seed"].append(torrent_hash)
def is_cross_seed(self, torrent):
"""Check if the torrent is a cross seed if it has one or more files that are cross seeded."""
t_hash = torrent.hash
t_name = torrent.name
if torrent.downloaded != 0:
logger.trace(f"Torrent: {t_name} [Hash: {t_hash}] is not a cross seeded torrent. Download is > 0.")
return False
cross_seed = True
for file in torrent.files:
file_name = file.name
if self.torrentfiles[file_name]["original"] == t_hash or t_hash not in self.torrentfiles[file_name]["cross_seed"]:
logger.trace(f"File: [{file_name}] is found in Torrent: {t_name} [Hash: {t_hash}] as the original torrent")
cross_seed = False
break
elif self.torrentfiles[file_name]["original"] is None:
cross_seed = False
break
logger.trace(f"Torrent: {t_name} [Hash: {t_hash}] {'is' if cross_seed else 'is not'} a cross seed torrent.")
return cross_seed
def has_cross_seed(self, torrent):
"""Check if the torrent has a cross seed"""
cross_seed = False
t_hash = torrent.hash
t_name = torrent.name
for file in torrent.files:
file_name = file.name
if len(self.torrentfiles[file_name]["cross_seed"]) > 0:
logger.trace(f"{file_name} has cross seeds: {self.torrentfiles[file_name]['cross_seed']}")
cross_seed = True
break
logger.trace(f"Torrent: {t_name} [Hash: {t_hash}] {'has' if cross_seed else 'has no'} cross seeds.")
return cross_seed
def remove_torrent_files(self, torrent):
"""Update the torrent_files list after a torrent is deleted"""
torrent_hash = torrent.hash
for file in torrent.files:
file_name = file.name
if self.torrentfiles[file_name]["original"] == torrent_hash:
if len(self.torrentfiles[file_name]["cross_seed"]) > 0:
self.torrentfiles[file_name]["original"] = self.torrentfiles[file_name]["cross_seed"].pop(0)
logger.trace(f"Updated {file_name} original to {self.torrentfiles[file_name]['original']}")
else:
self.torrentfiles[file_name]["original"] = None
else:
if torrent_hash in self.torrentfiles[file_name]["cross_seed"]:
self.torrentfiles[file_name]["cross_seed"].remove(torrent_hash)
logger.trace(f"Removed {torrent_hash} from {file_name} cross seeds")
logger.trace(f"{file_name} original: {self.torrentfiles[file_name]['original']}")
logger.trace(f"{file_name} cross seeds: {self.torrentfiles[file_name]['cross_seed']}")
def get_torrents(self, params): def get_torrents(self, params):
"""Get torrents from qBittorrent""" """Get torrents from qBittorrent"""
return self.client.torrents.info(**params) return self.client.torrents.info(**params)
@ -318,6 +380,11 @@ class Qbt:
def tor_delete_recycle(self, torrent, info): def tor_delete_recycle(self, torrent, info):
"""Move torrent to recycle bin""" """Move torrent to recycle bin"""
try:
self.remove_torrent_files(torrent)
except ValueError:
logger.debug(f"Torrent {torrent.name} has already been removed from torrent files.")
if self.config.recyclebin["enabled"]: if self.config.recyclebin["enabled"]:
tor_files = [] tor_files = []
try: try:

View file

@ -1,4 +1,5 @@
""" Utility functions for qBit Manage. """ """ Utility functions for qBit Manage. """
import json import json
import logging import logging
import os import os
@ -276,7 +277,7 @@ class check:
elif var_type == "float": elif var_type == "float":
try: try:
data[attribute] = float(data[attribute]) data[attribute] = float(data[attribute])
except: except Exception:
pass pass
if isinstance(data[attribute], float) and data[attribute] >= min_int: if isinstance(data[attribute], float) and data[attribute] >= min_int:
return data[attribute] return data[attribute]

View file

@ -1,4 +1,5 @@
"""Class to handle webhooks.""" """Class to handle webhooks."""
import time import time
from json import JSONDecodeError from json import JSONDecodeError

View file

@ -1,4 +1,4 @@
#!/usr/bin/python3 #!/usr/bin/env python3
"""qBittorrent Manager.""" """qBittorrent Manager."""
import argparse import argparse
import glob import glob
@ -11,6 +11,7 @@ from datetime import timedelta
try: try:
import schedule import schedule
from modules.logs import MyLogger from modules.logs import MyLogger
except ModuleNotFoundError: except ModuleNotFoundError:
print("Requirements Error: Requirements are not installed") print("Requirements Error: Requirements are not installed")
@ -231,7 +232,8 @@ def get_arg(env_str, default, arg_bool=False, arg_int=False):
try: try:
from git import Repo, InvalidGitRepositoryError from git import InvalidGitRepositoryError
from git import Repo
try: try:
git_branch = Repo(path=".").head.ref.name # noqa git_branch = Repo(path=".").head.ref.name # noqa
@ -340,16 +342,16 @@ from modules import util # noqa
util.logger = logger 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 Failed # noqa
from modules.core.category import Category # 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.cross_seed import CrossSeed # noqa
from modules.core.recheck import ReCheck # noqa from modules.core.recheck import ReCheck # noqa
from modules.core.tag_nohardlinks import TagNoHardLinks # noqa
from modules.core.remove_orphaned import RemoveOrphaned # noqa from modules.core.remove_orphaned import RemoveOrphaned # noqa
from modules.core.remove_unregistered import RemoveUnregistered # noqa
from modules.core.share_limits import ShareLimits # noqa from modules.core.share_limits import ShareLimits # noqa
from modules.core.tag_nohardlinks import TagNoHardLinks # noqa
from modules.core.tags import Tags # noqa
from modules.util import Failed # noqa
from modules.util import GracefulKiller # noqa
def my_except_hook(exctype, value, tbi): def my_except_hook(exctype, value, tbi):

View file

@ -1,2 +1,2 @@
flake8==7.0.0 flake8==7.0.0
pre-commit==3.6.0 pre-commit==3.6.2

View file

@ -1,7 +1,7 @@
bencodepy==0.9.5 bencodepy==0.9.5
GitPython==3.1.41 GitPython==3.1.42
qbittorrent-api==2024.1.58 qbittorrent-api==2024.2.59
requests==2.31.0 requests==2.31.0
retrying==1.3.4 retrying==1.3.4
ruamel.yaml==0.18.5 ruamel.yaml==0.18.6
schedule==1.2.1 schedule==1.2.1

View file

@ -4,13 +4,13 @@ You can also allow incomplete torrents to be deleted.
Torrents will be deleted starting with the ones with the most seeds, only torrents with a single hardlink will be deleted. Torrents will be deleted starting with the ones with the most seeds, only torrents with a single hardlink will be deleted.
Only torrents on configured drive path will be deleted. To monitor multiple drives, use multiple copies of this script. Only torrents on configured drive path will be deleted. To monitor multiple drives, use multiple copies of this script.
""" """
import os import os
import shutil import shutil
import time import time
import qbittorrentapi import qbittorrentapi
"""===Config===""" """===Config==="""
# qBittorrent WebUi Login # qBittorrent WebUi Login
qbt_login = {"host": "localhost", "port": 8080, "username": "???", "password": "???"} qbt_login = {"host": "localhost", "port": 8080, "username": "???", "password": "???"}

View file

@ -1,4 +1,4 @@
#!/usr/bin/python3 #!/usr/bin/env python3
# This standalone script is used to edit tracker urls from one tracker to another. # This standalone script is used to edit tracker urls from one tracker to another.
# Needs to have qbittorrent-api installed # Needs to have qbittorrent-api installed
# pip3 install qbittorrent-api # pip3 install qbittorrent-api
@ -14,7 +14,9 @@ NEW_TRACKER = "https://blutopia.cc" # This is the tracker you want to replace i
# --START SCRIPT--# # --START SCRIPT--#
try: try:
from qbittorrentapi import Client, LoginFailed, APIConnectionError from qbittorrentapi import APIConnectionError
from qbittorrentapi import Client
from qbittorrentapi import LoginFailed
except ModuleNotFoundError: except ModuleNotFoundError:
print('Requirements Error: qbittorrent-api not installed. Please install using the command "pip install qbittorrent-api"') print('Requirements Error: qbittorrent-api not installed. Please install using the command "pip install qbittorrent-api"')
sys.exit(1) sys.exit(1)

View file

@ -12,28 +12,45 @@ parser = argparse.ArgumentParser(prog="Qbit Mover", description="Stop torrents a
parser.add_argument("--host", help="qbittorrent host including port", required=True) parser.add_argument("--host", help="qbittorrent host including port", required=True)
parser.add_argument("-u", "--user", help="qbittorrent user", default="admin") parser.add_argument("-u", "--user", help="qbittorrent user", default="admin")
parser.add_argument("-p", "--password", help="qbittorrent password", default="adminadmin") parser.add_argument("-p", "--password", help="qbittorrent password", default="adminadmin")
parser.add_argument("--days_from", help="Set Number of Days to stop torrents between two offsets", type=int, default=0) parser.add_argument(
parser.add_argument("--days_to", help="Set Number of Days to stop torrents between two offsets", type=int, default=2) "--cache-mount",
"--cache_mount",
help="Cache mount point in Unraid. This is used to additionally filter for only torrents that exists on the cache mount."
"Use this option ONLY if you follow TRaSH Guides folder structure.",
default=None,
)
parser.add_argument(
"--days-from", "--days_from", help="Set Number of Days to stop torrents between two offsets", type=int, default=0
)
parser.add_argument("--days-to", "--days_to", help="Set Number of Days to stop torrents between two offsets", type=int, default=2)
# --DEFINE VARIABLES--# # --DEFINE VARIABLES--#
# --START SCRIPT--# # --START SCRIPT--#
try: try:
from qbittorrentapi import Client, LoginFailed, APIConnectionError from qbittorrentapi import APIConnectionError
from qbittorrentapi import Client
from qbittorrentapi import LoginFailed
except ModuleNotFoundError: except ModuleNotFoundError:
print('Requirements Error: qbittorrent-api not installed. Please install using the command "pip install qbittorrent-api"') print('Requirements Error: qbittorrent-api not installed. Please install using the command "pip install qbittorrent-api"')
sys.exit(1) sys.exit(1)
def filter_torrents(torrent_list, timeoffset_from, timeoffset_to): def filter_torrents(torrent_list, timeoffset_from, timeoffset_to, cache_mount):
result = [] result = []
for torrent in torrent_list: for torrent in torrent_list:
if torrent.added_on >= timeoffset_to and torrent.added_on <= timeoffset_from: if torrent.added_on >= timeoffset_to and torrent.added_on <= timeoffset_from:
if not cache_mount or exists_in_cache(cache_mount, torrent.content_path):
result.append(torrent) result.append(torrent)
elif torrent.added_on < timeoffset_to: elif torrent.added_on < timeoffset_to:
break break
return result return result
def exists_in_cache(cache_mount, content_path):
cache_path = os.path.join(cache_mount, content_path.lstrip("/"))
return os.path.exists(cache_path)
def stop_start_torrents(torrent_list, pause=True): def stop_start_torrents(torrent_list, pause=True):
for torrent in torrent_list: for torrent in torrent_list:
if pause: if pause:
@ -64,7 +81,7 @@ if __name__ == "__main__":
timeoffset_to = current - timedelta(days=args.days_to) timeoffset_to = current - timedelta(days=args.days_to)
torrent_list = client.torrents.info(sort="added_on", reverse=True) torrent_list = client.torrents.info(sort="added_on", reverse=True)
torrents = filter_torrents(torrent_list, timeoffset_from.timestamp(), timeoffset_to.timestamp()) torrents = filter_torrents(torrent_list, timeoffset_from.timestamp(), timeoffset_to.timestamp(), args.cache_mount)
# Pause Torrents # Pause Torrents
print(f"Pausing [{len(torrents)}] torrents from {args.days_from} - {args.days_to} days ago") print(f"Pausing [{len(torrents)}] torrents from {args.days_from} - {args.days_to} days ago")

View file

@ -1,9 +1,7 @@
#!/bin/bash #!/bin/bash
staged_changes=$(git diff-index --cached HEAD | wc -l | awk '{print $1}')
# Check if there are any changes staged for commit # Check if there are any changes staged for commit
if [ "$staged_changes" -eq 0 ]; then if [[ -z $(git diff --cached --name-only) ]]; then
echo "There are no changes staged for commit. Skipping version update." echo "There are no changes staged for commit. Skipping version update."
exit 0 exit 0
fi fi
@ -15,19 +13,17 @@ if git diff --cached --name-only | grep -q "VERSION"; then
fi fi
# Read the current version from the VERSION file # Read the current version from the VERSION file
current_version=$(cat VERSION) current_version=$(<VERSION)
echo "Current version: $current_version" echo "Current version: $current_version"
# Check if "develop" is not present in the version string # Check if "develop" is not present in the version string
if [[ $current_version != *"develop"* ]]; then if [[ $current_version != *"develop"* ]]; then
echo "The word 'develop' is not present in the version string." echo "The word 'develop' is not present in the version string."
exit 0 exit 0
fi fi
# Get the version number from the HEAD commit
current_version=$(git show HEAD:VERSION 2>/dev/null)
# Extract the version number after "develop" # Extract the version number after "develop"
version_number=$(echo "$current_version" | grep -oP '(?<=develop)\d+') version_number=$(echo "$current_version" | sed -n 's/.*develop\([0-9]*\).*/\1/p')
# Increment the version number # Increment the version number
new_version_number=$((version_number + 1)) new_version_number=$((version_number + 1))
@ -36,6 +32,6 @@ new_version_number=$((version_number + 1))
new_version=$(echo "$current_version" | sed "s/develop$version_number/develop$new_version_number/") new_version=$(echo "$current_version" | sed "s/develop$version_number/develop$new_version_number/")
# Update the VERSION file # Update the VERSION file
echo "$new_version" > VERSION sed -i "s/$current_version/$new_version/" VERSION
echo "Version updated to: $new_version" echo "Version updated to: $new_version"

View file

@ -1,8 +1,10 @@
import os import os
from distutils.core import setup from distutils.core import setup
from setuptools import find_packages from setuptools import find_packages
from modules import __version__
# User-friendly description from README.md # User-friendly description from README.md
current_directory = os.path.dirname(os.path.abspath(__file__)) current_directory = os.path.dirname(os.path.abspath(__file__))
try: try:
@ -11,20 +13,17 @@ try:
except Exception: except Exception:
long_description = "" long_description = ""
try:
with open(os.path.join(current_directory, "VERSION"), encoding="utf-8") as f:
version_no = f.read()
except Exception:
version_no = ""
setup( setup(
# Name of the package # Name of the package
name="qbit_manage", name="qbit_manage",
# Packages to include into the distribution # Packages to include into the distribution
packages=find_packages("."), packages=find_packages("."),
package_data={"": ["../*"]},
include_package_data=True,
# Start with a small number and increase it with # Start with a small number and increase it with
# every change you make https://semver.org # every change you make https://semver.org
version=version_no, version=__version__,
# Chose a license from here: https: // # Chose a license from here: https: //
# help.github.com / articles / licensing - a - # help.github.com / articles / licensing - a -
# repository. For example: MIT # repository. For example: MIT

View file

@ -32,3 +32,8 @@ max-line-length = 130
[pep8] [pep8]
extend-ignore = E722,E402 extend-ignore = E722,E402
[tool.isort]
add_imports = ["from __future__ import annotations"]
force_single_line = true
profile = "black"