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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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.
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": "???"}

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.
# 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)

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("-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")

View file

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

View file

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

View file

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