mirror of
https://github.com/StuffAnThings/qbit_manage.git
synced 2025-10-29 07:26:25 +08:00
commit
29690e9581
29 changed files with 334 additions and 133 deletions
16
.github/workflows/develop.yml
vendored
16
.github/workflows/develop.yml
vendored
|
|
@ -12,6 +12,11 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: set lower case owner name
|
||||
run: |
|
||||
echo "OWNER_LC=${OWNER,,}" >>${GITHUB_ENV}
|
||||
env:
|
||||
OWNER: '${{ github.repository_owner }}'
|
||||
|
||||
- name: Check Out Repo
|
||||
uses: actions/checkout@v4
|
||||
|
|
@ -24,6 +29,13 @@ jobs:
|
|||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
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
|
||||
uses: docker/setup-qemu-action@master
|
||||
with:
|
||||
|
|
@ -43,7 +55,9 @@ jobs:
|
|||
"BRANCH_NAME=develop"
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
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
|
||||
uses: joelwmale/webhook-action@master
|
||||
|
|
|
|||
30
.github/workflows/latest.yml
vendored
30
.github/workflows/latest.yml
vendored
|
|
@ -10,23 +10,28 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: set lower case owner name
|
||||
run: |
|
||||
echo "OWNER_LC=${OWNER,,}" >>${GITHUB_ENV}
|
||||
env:
|
||||
OWNER: '${{ github.repository_owner }}'
|
||||
|
||||
- name: Check Out Repo
|
||||
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
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
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
|
||||
uses: docker/setup-qemu-action@master
|
||||
with:
|
||||
|
|
@ -44,4 +49,13 @@ jobs:
|
|||
file: ./Dockerfile
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
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": "" }'
|
||||
|
|
|
|||
17
.github/workflows/version.yml
vendored
17
.github/workflows/version.yml
vendored
|
|
@ -11,6 +11,11 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: set lower case owner name
|
||||
run: |
|
||||
echo "OWNER_LC=${OWNER,,}" >>${GITHUB_ENV}
|
||||
env:
|
||||
OWNER: '${{ github.repository_owner }}'
|
||||
|
||||
- name: Check Out Repo
|
||||
uses: actions/checkout@v4
|
||||
|
|
@ -23,6 +28,13 @@ jobs:
|
|||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
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
|
||||
uses: docker/setup-qemu-action@master
|
||||
with:
|
||||
|
|
@ -44,8 +56,9 @@ jobs:
|
|||
file: ./Dockerfile
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
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
|
||||
id: create_release
|
||||
uses: softprops/action-gh-release@v1
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ repos:
|
|||
hooks:
|
||||
- id: autopep8
|
||||
- repo: https://github.com/adrienverge/yamllint.git
|
||||
rev: v1.33.0 # or higher tag
|
||||
rev: v1.35.1 # or higher tag
|
||||
hooks:
|
||||
- id: yamllint
|
||||
args: [--format, parsable, --strict]
|
||||
|
|
@ -31,17 +31,19 @@ repos:
|
|||
hooks:
|
||||
- id: yamlfix
|
||||
exclude: ^.github/
|
||||
- repo: https://github.com/asottile/reorder-python-imports
|
||||
rev: v3.12.0
|
||||
- repo: https://github.com/pycqa/isort
|
||||
rev: 5.13.2
|
||||
hooks:
|
||||
- id: reorder-python-imports
|
||||
- id: isort
|
||||
name: isort (python)
|
||||
args: [--force-single-line-imports, --profile, black]
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.15.0
|
||||
rev: v3.15.1
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py3-plus]
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 23.12.1
|
||||
rev: 24.2.0
|
||||
hooks:
|
||||
- id: black
|
||||
language_version: python3
|
||||
|
|
|
|||
24
CHANGELOG
24
CHANGELOG
|
|
@ -1,8 +1,22 @@
|
|||
# Requirements Updated
|
||||
- qbittorrent-api==2024.1.58
|
||||
- qbittorrent-api==2024.2.59
|
||||
- GitPython==3.1.42
|
||||
- ruamel.yaml==0.18.6
|
||||
|
||||
# Updates
|
||||
- Adds arguments for mover script (Adds #473)
|
||||
# New Features
|
||||
- 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
|
||||
|
|
|
|||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
4.0.8
|
||||
4.0.9
|
||||
|
|
|
|||
|
|
@ -29,10 +29,20 @@ settings:
|
|||
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_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.
|
||||
- noHL
|
||||
- issue
|
||||
- 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:
|
||||
# Do not remove these
|
||||
# Cross-seed var: </your/path/here/> # Output directory of cross-seed
|
||||
|
|
@ -172,10 +182,10 @@ share_limits:
|
|||
categories:
|
||||
- RadarrComplete
|
||||
- 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.
|
||||
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.
|
||||
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).
|
||||
|
|
@ -188,14 +198,14 @@ share_limits:
|
|||
last_active: 43200
|
||||
# <OPTIONAL> Limit Upload Speed <int>: Will limit the upload speed KiB/s (KiloBytes/second) (`-1` : No Limit)
|
||||
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
|
||||
# <OPTIONAL> resume_torrent_after_change <bool>: This variable will resume your torrent after changing share limits. Default is 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
|
||||
# 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
|
||||
# <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.
|
||||
# Will default to 0 if not specified for the group.
|
||||
min_num_seeds: 0
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
"""Apprise notification class"""
|
||||
|
||||
import time
|
||||
|
||||
from modules import util
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
"""Module for BeyondHD (BHD) tracker."""
|
||||
|
||||
from json import JSONDecodeError
|
||||
|
||||
from modules import util
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
"""Config class for qBittorrent-Manage"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import stat
|
||||
|
|
@ -13,9 +14,9 @@ from modules.apprise import Apprise
|
|||
from modules.bhd import BeyondHD
|
||||
from modules.notifiarr import Notifiarr
|
||||
from modules.qbittorrent import Qbt
|
||||
from modules.util import check
|
||||
from modules.util import Failed
|
||||
from modules.util import YAML
|
||||
from modules.util import Failed
|
||||
from modules.util import check
|
||||
from modules.webhooks import Webhooks
|
||||
|
||||
logger = util.logger
|
||||
|
|
@ -167,17 +168,47 @@ class Config:
|
|||
"share_limits_tag": self.util.check_for_attribute(
|
||||
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.nohardlinks_tag = self.settings["nohardlinks_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.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"]:
|
||||
self.util.overwrite_attributes(self.settings, "settings")
|
||||
|
||||
|
|
@ -280,6 +311,8 @@ class Config:
|
|||
cat_str = list(cat.keys())[0]
|
||||
self.nohardlinks[cat_str] = {}
|
||||
exclude_tags = cat[cat_str].get("exclude_tags", [])
|
||||
if exclude_tags is None:
|
||||
exclude_tags = []
|
||||
if isinstance(exclude_tags, str):
|
||||
exclude_tags = [exclude_tags]
|
||||
self.nohardlinks[cat_str]["exclude_tags"] = exclude_tags
|
||||
|
|
@ -687,7 +720,8 @@ class Config:
|
|||
if num_del > 0:
|
||||
if not self.dry_run:
|
||||
for path in location_path_list:
|
||||
util.remove_empty_directories(path, "**/*")
|
||||
if path != location_path:
|
||||
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}.",
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ class Category:
|
|||
self.torrents_updated = [] # List of torrents updated
|
||||
self.notify_attr = [] # List of single torrent attributes to send to notifiarr
|
||||
self.uncategorized_mapping = "Uncategorized"
|
||||
self.status_filter = "completed" if self.config.settings["cat_filter_completed"] else "all"
|
||||
|
||||
self.category()
|
||||
self.config.webhooks_factory.notify(self.torrents_updated, self.notify_attr, group_by="category")
|
||||
|
|
@ -21,7 +22,7 @@ class 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"})
|
||||
torrent_list = self.qbt.get_torrents({"category": "", "status_filter": self.status_filter})
|
||||
for torrent in torrent_list:
|
||||
new_cat = self.get_tracker_cat(torrent) or self.qbt.get_category(torrent.save_path)
|
||||
if new_cat == self.uncategorized_mapping:
|
||||
|
|
@ -32,7 +33,7 @@ class Category:
|
|||
# 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"})
|
||||
torrent_list = self.qbt.get_torrents({"category": old_cat, "status_filter": self.status_filter})
|
||||
for torrent in torrent_list:
|
||||
new_cat = self.config.cat_change[old_cat]
|
||||
self.update_cat(torrent, new_cat, True)
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ class CrossSeed:
|
|||
self.client = qbit_manager.client
|
||||
self.stats_added = 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.notify_attr = [] # List of single torrent attributes to send to notifiarr
|
||||
|
|
@ -40,8 +41,8 @@ class CrossSeed:
|
|||
# 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)
|
||||
file_cs_out = os.path.join(dir_cs_out, file)
|
||||
file_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))
|
||||
|
|
@ -65,7 +66,7 @@ class CrossSeed:
|
|||
"torrents": [t_name],
|
||||
"torrent_category": category,
|
||||
"torrent_save_path": dest,
|
||||
"torrent_tag": "cross-seed",
|
||||
"torrent_tag": self.cross_seed_tag,
|
||||
"torrent_tracker": t_tracker,
|
||||
}
|
||||
self.notify_attr.append(attr)
|
||||
|
|
@ -73,13 +74,12 @@ class CrossSeed:
|
|||
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
|
||||
torrent_files=src, save_path=dest, category=category, tags=self.cross_seed_tag, is_paused=True
|
||||
)
|
||||
self.qbt.torrentinfo[t_name]["count"] += 1
|
||||
try:
|
||||
torrent_hash_generator = TorrentHashGenerator(src)
|
||||
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:
|
||||
logger.warning(f"Unable to generate torrent hash from cross-seed {t_name}: {e}")
|
||||
try:
|
||||
|
|
@ -89,6 +89,7 @@ class CrossSeed:
|
|||
logger.warning(f"Unable to find hash {torrent_hash} in qbt: {e}")
|
||||
if torrent_info:
|
||||
torrent = torrent_info[0]
|
||||
self.qbt.add_torrent_files(torrent.hash, torrent.files)
|
||||
self.qbt.torrentvalid.append(torrent)
|
||||
self.qbt.torrentinfo[t_name]["torrents"].append(torrent)
|
||||
self.qbt.torrent_list.append(torrent)
|
||||
|
|
@ -101,7 +102,7 @@ class CrossSeed:
|
|||
logger.print_line(error, self.config.loglevel)
|
||||
else:
|
||||
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.webhooks_factory.notify(self.torrents_updated, self.notify_attr, group_by="category")
|
||||
|
|
@ -112,14 +113,16 @@ class CrossSeed:
|
|||
t_name = torrent.name
|
||||
t_cat = torrent.category
|
||||
if (
|
||||
not util.is_tag_in_torrent("cross-seed", torrent.tags)
|
||||
and self.qbt.torrentinfo[t_name]["count"] > 1
|
||||
and self.qbt.torrentinfo[t_name]["first_hash"] != torrent.hash
|
||||
not util.is_tag_in_torrent(self.cross_seed_tag, torrent.tags)
|
||||
and self.qbt.is_cross_seed(torrent)
|
||||
and torrent.downloaded == 0
|
||||
and torrent.seeding_time > 0
|
||||
):
|
||||
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
|
||||
f"{'Not Adding' if self.config.dry_run else 'Adding'} '{self.cross_seed_tag}' tag to {t_name}",
|
||||
self.config.loglevel,
|
||||
)
|
||||
attr = {
|
||||
"function": "tag_cross_seed",
|
||||
|
|
@ -127,13 +130,13 @@ class CrossSeed:
|
|||
"body": body,
|
||||
"torrents": [t_name],
|
||||
"torrent_category": t_cat,
|
||||
"torrent_tag": "cross-seed",
|
||||
"torrent_tag": self.cross_seed_tag,
|
||||
"torrent_tracker": tracker["url"],
|
||||
}
|
||||
self.notify_attr.append(attr)
|
||||
self.torrents_updated.append(t_name)
|
||||
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")
|
||||
numcategory = Counter(categories)
|
||||
for cat in numcategory:
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ from qbittorrentapi import NotFound404Error
|
|||
from qbittorrentapi import TrackerStatus
|
||||
|
||||
from modules import util
|
||||
from modules.util import list_in_text
|
||||
from modules.util import TorrentMessages
|
||||
from modules.util import list_in_text
|
||||
|
||||
logger = util.logger
|
||||
|
||||
|
|
@ -209,7 +209,7 @@ class RemoveUnregistered:
|
|||
"torrent_tracker": tracker["url"],
|
||||
"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
|
||||
if "" in self.t_msg or 2 in self.t_status:
|
||||
attr["torrents_deleted_and_contents"] = False
|
||||
|
|
@ -232,4 +232,3 @@ class RemoveUnregistered:
|
|||
attr["body"] = "\n".join(body)
|
||||
self.torrents_updated_unreg.append(self.t_name)
|
||||
self.notify_attr_unreg.append(attr)
|
||||
self.qbt.torrentinfo[self.t_name]["count"] -= 1
|
||||
|
|
|
|||
|
|
@ -8,10 +8,6 @@ from modules.webhooks import GROUP_NOTIFICATION_LIMIT
|
|||
|
||||
logger = util.logger
|
||||
|
||||
MIN_SEEDING_TIME_TAG = "MinSeedTimeNotReached"
|
||||
MIN_NUM_SEEDS_TAG = "MinSeedsNotMet"
|
||||
LAST_ACTIVE_TAG = "LastActiveLimitNotReached"
|
||||
|
||||
|
||||
class ShareLimits:
|
||||
def __init__(self, qbit_manager):
|
||||
|
|
@ -23,6 +19,7 @@ class ShareLimits:
|
|||
# meets the criteria for ratio limit/seed limit for deletion
|
||||
self.stats_deleted_contents = 0 # counter for the number of torrents that \
|
||||
# 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.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.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.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.update_share_limits()
|
||||
|
|
@ -39,7 +39,7 @@ class ShareLimits:
|
|||
def update_share_limits(self):
|
||||
"""Updates share limits for torrents based on grouping"""
|
||||
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)
|
||||
for group_name, group_config in self.share_limits_config.items():
|
||||
torrents = group_config["torrents"]
|
||||
|
|
@ -79,7 +79,6 @@ class ShareLimits:
|
|||
for torrent_hash, torrent_dict in self.tdel_dict.items():
|
||||
torrent = torrent_dict["torrent"]
|
||||
t_name = torrent.name
|
||||
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
|
||||
|
|
@ -105,7 +104,7 @@ class ShareLimits:
|
|||
}
|
||||
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):
|
||||
if self.qbt.has_cross_seed(torrent) and ("" in t_msg or 2 in t_status):
|
||||
self.stats_deleted += 1
|
||||
attr["torrents_deleted_and_contents"] = False
|
||||
t_deleted.add(t_name)
|
||||
|
|
@ -136,7 +135,6 @@ class ShareLimits:
|
|||
attr["body"] = "\n".join(body)
|
||||
if not group_notifications:
|
||||
self.config.send_notifications(attr)
|
||||
self.qbt.torrentinfo[t_name]["count"] -= 1
|
||||
if group_notifications:
|
||||
if t_deleted:
|
||||
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
|
||||
) and hash_not_prev_checked:
|
||||
if (
|
||||
not is_tag_in_torrent(MIN_SEEDING_TIME_TAG, torrent.tags)
|
||||
and not is_tag_in_torrent(MIN_NUM_SEEDS_TAG, torrent.tags)
|
||||
and not is_tag_in_torrent(LAST_ACTIVE_TAG, torrent.tags)
|
||||
not is_tag_in_torrent(self.min_seeding_time_tag, torrent.tags)
|
||||
and not is_tag_in_torrent(self.min_num_seeds_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'Tracker: {tracker["url"]}', 8), self.config.loglevel)
|
||||
|
|
@ -373,11 +371,11 @@ class ShareLimits:
|
|||
max_ratio = torrent.max_ratio
|
||||
if max_seeding_time is None:
|
||||
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 []
|
||||
if is_tag_in_torrent(MIN_NUM_SEEDS_TAG, torrent.tags):
|
||||
if is_tag_in_torrent(self.min_num_seeds_tag, torrent.tags):
|
||||
return []
|
||||
if is_tag_in_torrent(LAST_ACTIVE_TAG, torrent.tags):
|
||||
if is_tag_in_torrent(self.last_active_tag, torrent.tags):
|
||||
return []
|
||||
torrent.set_share_limits(ratio_limit=max_ratio, seeding_time_limit=max_seeding_time, inactive_seeding_time_limit=-2)
|
||||
return body
|
||||
|
|
@ -391,12 +389,12 @@ class ShareLimits:
|
|||
def _has_reached_min_seeding_time_limit():
|
||||
print_log = []
|
||||
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:
|
||||
torrent.remove_tags(tags=MIN_SEEDING_TIME_TAG)
|
||||
torrent.remove_tags(tags=self.min_seeding_time_tag)
|
||||
return True
|
||||
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"Tracker: {tracker}", 8), self.config.loglevel)
|
||||
print_log += logger.print_line(
|
||||
|
|
@ -409,10 +407,10 @@ class ShareLimits:
|
|||
self.config.loglevel,
|
||||
)
|
||||
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:
|
||||
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)
|
||||
if resume_torrent:
|
||||
torrent.resume()
|
||||
|
|
@ -421,12 +419,12 @@ class ShareLimits:
|
|||
def _is_less_than_min_num_seeds():
|
||||
print_log = []
|
||||
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:
|
||||
torrent.remove_tags(tags=MIN_NUM_SEEDS_TAG)
|
||||
torrent.remove_tags(tags=self.min_num_seeds_tag)
|
||||
return False
|
||||
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"Tracker: {tracker}", 8), self.config.loglevel)
|
||||
print_log += logger.print_line(
|
||||
|
|
@ -439,10 +437,10 @@ class ShareLimits:
|
|||
self.config.loglevel,
|
||||
)
|
||||
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:
|
||||
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)
|
||||
if resume_torrent:
|
||||
torrent.resume()
|
||||
|
|
@ -453,12 +451,12 @@ class ShareLimits:
|
|||
now = int(time())
|
||||
inactive_time_minutes = round((now - torrent.last_activity) / 60)
|
||||
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:
|
||||
torrent.remove_tags(tags=LAST_ACTIVE_TAG)
|
||||
torrent.remove_tags(tags=self.last_active_tag)
|
||||
return True
|
||||
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"Tracker: {tracker}", 8), self.config.loglevel)
|
||||
print_log += logger.print_line(
|
||||
|
|
@ -470,9 +468,11 @@ class ShareLimits:
|
|||
),
|
||||
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:
|
||||
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)
|
||||
if resume_torrent:
|
||||
torrent.resume()
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ class TagNoHardLinks:
|
|||
self.torrents_updated_untagged = [] # List of torrents updated
|
||||
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.config.webhooks_factory.notify(self.torrents_updated_tagged, self.notify_attr_tagged, group_by="tag")
|
||||
|
|
@ -87,7 +89,7 @@ class TagNoHardLinks:
|
|||
nohardlinks = self.nohardlinks
|
||||
check_hardlinks = util.CheckHardLinks(self.root_dir, self.remote_dir)
|
||||
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:
|
||||
ex = (
|
||||
"No torrents found in the category ("
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ class Tags:
|
|||
self.client = qbit_manager.client
|
||||
self.stats = 0
|
||||
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.notify_attr = [] # List of single torrent attributes to send to notifiarr
|
||||
|
||||
|
|
@ -19,6 +20,7 @@ class Tags:
|
|||
def tags(self):
|
||||
"""Update tags for torrents"""
|
||||
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)
|
||||
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]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
"""Logging module"""
|
||||
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
"""Qbittorrent Module"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
|
@ -10,8 +11,8 @@ from qbittorrentapi import Version
|
|||
|
||||
from modules import util
|
||||
from modules.util import Failed
|
||||
from modules.util import list_in_text
|
||||
from modules.util import TorrentMessages
|
||||
from modules.util import list_in_text
|
||||
|
||||
logger = util.logger
|
||||
|
||||
|
|
@ -70,15 +71,16 @@ class Qbt:
|
|||
logger.print_line(ex, "CRITICAL")
|
||||
sys.exit(1)
|
||||
logger.info("Qbt Connection Successful")
|
||||
except LoginFailed as exc:
|
||||
except LoginFailed:
|
||||
ex = "Qbittorrent Error: Failed to login. Invalid username/password."
|
||||
self.config.notify(ex, "Qbittorrent")
|
||||
raise Failed(exc) from exc
|
||||
raise Failed(ex)
|
||||
except Exception as exc:
|
||||
self.config.notify(exc, "Qbittorrent")
|
||||
raise Failed(exc) from exc
|
||||
raise Failed(exc)
|
||||
logger.separator("Getting Torrent List", space=False, border=False)
|
||||
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 = self.client.app.preferences.max_ratio
|
||||
|
|
@ -96,12 +98,11 @@ class Qbt:
|
|||
def get_torrent_info(self):
|
||||
"""
|
||||
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':'[]'...},
|
||||
'TorrentName2' : {'Category':'Movies', 'save_path':'/data/torrents/Movies'}, 'count':2, 'msg':'[]'...}
|
||||
self.torrentinfo = {'TorrentName1' : {'Category':'TV', 'save_path':'/data/torrents/TV', 'msg':'[]'...},
|
||||
'TorrentName2' : {'Category':'Movies', 'save_path':'/data/torrents/Movies'}, 'msg':'[]'...}
|
||||
List of dictionary key definitions
|
||||
Category = Returns category 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)
|
||||
status = Returns the list of status numbers of the torrent by name
|
||||
(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)
|
||||
is_complete = Returns the state of torrent
|
||||
(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.torrentissue = [] # list of unregistered torrent objects
|
||||
|
|
@ -140,23 +139,20 @@ class Qbt:
|
|||
save_path = torrent.save_path
|
||||
category = torrent.category
|
||||
torrent_trackers = torrent.trackers
|
||||
self.add_torrent_files(torrent_hash, torrent.files)
|
||||
except Exception as ex:
|
||||
self.config.notify(ex, "Get Torrent Info", False)
|
||||
logger.warning(ex)
|
||||
if torrent_name in self.torrentinfo:
|
||||
t_obj_list.append(torrent)
|
||||
t_count = self.torrentinfo[torrent_name]["count"] + 1
|
||||
msg_list = self.torrentinfo[torrent_name]["msg"]
|
||||
status_list = self.torrentinfo[torrent_name]["status"]
|
||||
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:
|
||||
t_obj_list = [torrent]
|
||||
t_count = 1
|
||||
msg_list = []
|
||||
status_list = []
|
||||
is_complete = torrent_is_complete
|
||||
first_hash = torrent_hash
|
||||
for trk in torrent_trackers:
|
||||
if trk.url.startswith("http"):
|
||||
status = trk.status
|
||||
|
|
@ -187,14 +183,80 @@ class Qbt:
|
|||
"torrents": t_obj_list,
|
||||
"Category": category,
|
||||
"save_path": save_path,
|
||||
"count": t_count,
|
||||
"msg": msg_list,
|
||||
"status": status_list,
|
||||
"is_complete": is_complete,
|
||||
"first_hash": first_hash,
|
||||
}
|
||||
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):
|
||||
"""Get torrents from qBittorrent"""
|
||||
return self.client.torrents.info(**params)
|
||||
|
|
@ -318,6 +380,11 @@ class Qbt:
|
|||
|
||||
def tor_delete_recycle(self, torrent, info):
|
||||
"""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"]:
|
||||
tor_files = []
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
""" Utility functions for qBit Manage. """
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
|
@ -276,7 +277,7 @@ class check:
|
|||
elif var_type == "float":
|
||||
try:
|
||||
data[attribute] = float(data[attribute])
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
if isinstance(data[attribute], float) and data[attribute] >= min_int:
|
||||
return data[attribute]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
"""Class to handle webhooks."""
|
||||
|
||||
import time
|
||||
from json import JSONDecodeError
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
#!/usr/bin/python3
|
||||
#!/usr/bin/env python3
|
||||
"""qBittorrent Manager."""
|
||||
import argparse
|
||||
import glob
|
||||
|
|
@ -11,6 +11,7 @@ from datetime import timedelta
|
|||
|
||||
try:
|
||||
import schedule
|
||||
|
||||
from modules.logs import MyLogger
|
||||
except ModuleNotFoundError:
|
||||
print("Requirements Error: Requirements are not installed")
|
||||
|
|
@ -231,7 +232,8 @@ def get_arg(env_str, default, arg_bool=False, arg_int=False):
|
|||
|
||||
|
||||
try:
|
||||
from git import Repo, InvalidGitRepositoryError
|
||||
from git import InvalidGitRepositoryError
|
||||
from git import Repo
|
||||
|
||||
try:
|
||||
git_branch = Repo(path=".").head.ref.name # noqa
|
||||
|
|
@ -340,16 +342,16 @@ from modules import util # noqa
|
|||
|
||||
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
|
||||
from modules.core.remove_unregistered import RemoveUnregistered # 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):
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
flake8==7.0.0
|
||||
pre-commit==3.6.0
|
||||
pre-commit==3.6.2
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
bencodepy==0.9.5
|
||||
GitPython==3.1.41
|
||||
qbittorrent-api==2024.1.58
|
||||
GitPython==3.1.42
|
||||
qbittorrent-api==2024.2.59
|
||||
requests==2.31.0
|
||||
retrying==1.3.4
|
||||
ruamel.yaml==0.18.5
|
||||
ruamel.yaml==0.18.6
|
||||
schedule==1.2.1
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
Only torrents on configured drive path will be deleted. To monitor multiple drives, use multiple copies of this script.
|
||||
"""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import time
|
||||
|
||||
import qbittorrentapi
|
||||
|
||||
|
||||
"""===Config==="""
|
||||
# qBittorrent WebUi Login
|
||||
qbt_login = {"host": "localhost", "port": 8080, "username": "???", "password": "???"}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
# Needs to have qbittorrent-api installed
|
||||
# pip3 install qbittorrent-api
|
||||
|
|
@ -14,7 +14,9 @@ NEW_TRACKER = "https://blutopia.cc" # This is the tracker you want to replace i
|
|||
# --START SCRIPT--#
|
||||
|
||||
try:
|
||||
from qbittorrentapi import Client, LoginFailed, APIConnectionError
|
||||
from qbittorrentapi import APIConnectionError
|
||||
from qbittorrentapi import Client
|
||||
from qbittorrentapi import LoginFailed
|
||||
except ModuleNotFoundError:
|
||||
print('Requirements Error: qbittorrent-api not installed. Please install using the command "pip install qbittorrent-api"')
|
||||
sys.exit(1)
|
||||
|
|
|
|||
|
|
@ -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("-u", "--user", help="qbittorrent user", default="admin")
|
||||
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("--days_to", help="Set Number of Days to stop torrents between two offsets", type=int, default=2)
|
||||
parser.add_argument(
|
||||
"--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--#
|
||||
|
||||
# --START SCRIPT--#
|
||||
try:
|
||||
from qbittorrentapi import Client, LoginFailed, APIConnectionError
|
||||
from qbittorrentapi import APIConnectionError
|
||||
from qbittorrentapi import Client
|
||||
from qbittorrentapi import LoginFailed
|
||||
except ModuleNotFoundError:
|
||||
print('Requirements Error: qbittorrent-api not installed. Please install using the command "pip install qbittorrent-api"')
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def filter_torrents(torrent_list, timeoffset_from, timeoffset_to):
|
||||
def filter_torrents(torrent_list, timeoffset_from, timeoffset_to, cache_mount):
|
||||
result = []
|
||||
for torrent in torrent_list:
|
||||
if torrent.added_on >= timeoffset_to and torrent.added_on <= timeoffset_from:
|
||||
result.append(torrent)
|
||||
if not cache_mount or exists_in_cache(cache_mount, torrent.content_path):
|
||||
result.append(torrent)
|
||||
elif torrent.added_on < timeoffset_to:
|
||||
break
|
||||
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):
|
||||
for torrent in torrent_list:
|
||||
if pause:
|
||||
|
|
@ -64,7 +81,7 @@ if __name__ == "__main__":
|
|||
timeoffset_to = current - timedelta(days=args.days_to)
|
||||
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
|
||||
print(f"Pausing [{len(torrents)}] torrents from {args.days_from} - {args.days_to} days ago")
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
#!/bin/bash
|
||||
|
||||
staged_changes=$(git diff-index --cached HEAD | wc -l | awk '{print $1}')
|
||||
|
||||
# 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."
|
||||
exit 0
|
||||
fi
|
||||
|
|
@ -15,19 +13,17 @@ if git diff --cached --name-only | grep -q "VERSION"; then
|
|||
fi
|
||||
|
||||
# Read the current version from the VERSION file
|
||||
current_version=$(cat VERSION)
|
||||
current_version=$(<VERSION)
|
||||
echo "Current version: $current_version"
|
||||
|
||||
# Check if "develop" is not present in the version string
|
||||
if [[ $current_version != *"develop"* ]]; then
|
||||
echo "The word 'develop' is not present in the version string."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Get the version number from the HEAD commit
|
||||
current_version=$(git show HEAD:VERSION 2>/dev/null)
|
||||
|
||||
# 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
|
||||
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/")
|
||||
|
||||
# Update the VERSION file
|
||||
echo "$new_version" > VERSION
|
||||
sed -i "s/$current_version/$new_version/" VERSION
|
||||
|
||||
echo "Version updated to: $new_version"
|
||||
|
|
|
|||
13
setup.py
13
setup.py
|
|
@ -1,8 +1,10 @@
|
|||
import os
|
||||
|
||||
from distutils.core import setup
|
||||
|
||||
from setuptools import find_packages
|
||||
|
||||
from modules import __version__
|
||||
|
||||
# User-friendly description from README.md
|
||||
current_directory = os.path.dirname(os.path.abspath(__file__))
|
||||
try:
|
||||
|
|
@ -11,20 +13,17 @@ try:
|
|||
except Exception:
|
||||
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(
|
||||
# Name of the package
|
||||
name="qbit_manage",
|
||||
# Packages to include into the distribution
|
||||
packages=find_packages("."),
|
||||
package_data={"": ["../*"]},
|
||||
include_package_data=True,
|
||||
# Start with a small number and increase it with
|
||||
# every change you make https://semver.org
|
||||
version=version_no,
|
||||
version=__version__,
|
||||
# Chose a license from here: https: //
|
||||
# help.github.com / articles / licensing - a -
|
||||
# repository. For example: MIT
|
||||
|
|
|
|||
5
tox.ini
5
tox.ini
|
|
@ -32,3 +32,8 @@ max-line-length = 130
|
|||
|
||||
[pep8]
|
||||
extend-ignore = E722,E402
|
||||
|
||||
[tool.isort]
|
||||
add_imports = ["from __future__ import annotations"]
|
||||
force_single_line = true
|
||||
profile = "black"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue