diff --git a/.github/workflows/develop.yml b/.github/workflows/develop.yml index 6f3daee6..f8b52075 100644 --- a/.github/workflows/develop.yml +++ b/.github/workflows/develop.yml @@ -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 diff --git a/.github/workflows/latest.yml b/.github/workflows/latest.yml index 463257f2..ae28463a 100644 --- a/.github/workflows/latest.yml +++ b/.github/workflows/latest.yml @@ -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": "" }' diff --git a/.github/workflows/version.yml b/.github/workflows/version.yml index ae6c59e4..a9b79ec3 100644 --- a/.github/workflows/version.yml +++ b/.github/workflows/version.yml @@ -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 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 86d60458..4d19ebca 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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 diff --git a/CHANGELOG b/CHANGELOG index 9d95b9b7..98a21f60 100644 --- a/CHANGELOG +++ b/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 diff --git a/VERSION b/VERSION index a2cec7af..7919852f 100755 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.0.8 +4.0.9 diff --git a/config/config.yml.sample b/config/config.yml.sample index 48a78a2c..5f8b7e25 100755 --- a/config/config.yml.sample +++ b/config/config.yml.sample @@ -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: # Output directory of cross-seed @@ -172,10 +182,10 @@ share_limits: categories: - RadarrComplete - SonarrComplete - # max_ratio : Will set the torrent Maximum share ratio until torrent is stopped from seeding/uploading. + # max_ratio : 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 - # max_seeding_time : Will set the torrent Maximum seeding time (minutes) until torrent is stopped from seeding. + # max_seeding_time : 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 # min_seeding_time : 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 # Limit Upload Speed : Will limit the upload speed KiB/s (KiloBytes/second) (`-1` : No Limit) limit_upload_speed: 0 - # cleanup : WARNING!! Setting this as true Will remove and delete contents of any torrents that satisfies the share limits + # cleanup : 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 # resume_torrent_after_change : This variable will resume your torrent after changing share limits. Default is true resume_torrent_after_change: true # add_group_to_tag : 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 - # min_num_seeds : This will prevent torrent deletion by cleanup variable if the number of seeds is less than the value set here. + # min_num_seeds : 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 diff --git a/modules/apprise.py b/modules/apprise.py index 5ae9c0ec..697d557d 100755 --- a/modules/apprise.py +++ b/modules/apprise.py @@ -1,4 +1,5 @@ """Apprise notification class""" + import time from modules import util diff --git a/modules/bhd.py b/modules/bhd.py index 19d517d9..9bd21093 100755 --- a/modules/bhd.py +++ b/modules/bhd.py @@ -1,4 +1,5 @@ """Module for BeyondHD (BHD) tracker.""" + from json import JSONDecodeError from modules import util diff --git a/modules/config.py b/modules/config.py index adeb3b3e..a45fc136 100755 --- a/modules/config.py +++ b/modules/config.py @@ -1,4 +1,5 @@ """Config class for qBittorrent-Manage""" + import os import re import stat @@ -13,9 +14,9 @@ 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 @@ def hooks(attr): "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"] - - default_ignore_tags = [self.nohardlinks_tag, self.tracker_error_tag, "cross-seed"] + 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"] + + 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 @@ def hooks(attr): 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 @@ def cleanup_dirs(self, location): 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}.", diff --git a/modules/core/category.py b/modules/core/category.py index f2d50c38..2077584a 100644 --- a/modules/core/category.py +++ b/modules/core/category.py @@ -14,6 +14,7 @@ def __init__(self, qbit_manager): 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 @@ def __init__(self, qbit_manager): 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 @@ def category(self): # 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) diff --git a/modules/core/cross_seed.py b/modules/core/cross_seed.py index d841d5ab..68cffbae 100644 --- a/modules/core/cross_seed.py +++ b/modules/core/cross_seed.py @@ -14,6 +14,7 @@ def __init__(self, qbit_manager): 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 @@ def cross_seed(self): # 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 @@ def cross_seed(self): "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 @@ def cross_seed(self): 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 @@ def cross_seed(self): 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 @@ def cross_seed(self): 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 @@ def cross_seed(self): 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 @@ def cross_seed(self): "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: diff --git a/modules/core/remove_unregistered.py b/modules/core/remove_unregistered.py index a9fd30a2..97b8617e 100644 --- a/modules/core/remove_unregistered.py +++ b/modules/core/remove_unregistered.py @@ -2,8 +2,8 @@ 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 @@ def del_unregistered(self, msg, tracker, torrent): "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 @@ def del_unregistered(self, msg, tracker, torrent): 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 diff --git a/modules/core/share_limits.py b/modules/core/share_limits.py index f7ef28d4..5a71d9ad 100644 --- a/modules/core/share_limits.py +++ b/modules/core/share_limits.py @@ -8,10 +8,6 @@ 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 @@ def __init__(self, qbit_manager): # 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 @@ def __init__(self, qbit_manager): 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 @@ def __init__(self, qbit_manager): 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 @@ def cleanup_torrents_for_group(self, group_name, priority): 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 @@ def cleanup_torrents_for_group(self, group_name, priority): } 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 @@ def cleanup_torrents_for_group(self, group_name, priority): 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 @@ def update_share_limits_for_group(self, group_name, group_config, torrents): 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 @@ def set_tags_and_limits(self, torrent, max_ratio, max_seeding_time, limit_upload 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 @@ def has_reached_seed_limit( 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 @@ def _has_reached_min_seeding_time_limit(): 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 @@ def _has_reached_min_seeding_time_limit(): 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 @@ def _is_less_than_min_num_seeds(): 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 @@ def _has_reached_last_active_time_limit(): 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 @@ def _has_reached_last_active_time_limit(): ), 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() diff --git a/modules/core/tag_nohardlinks.py b/modules/core/tag_nohardlinks.py index d4a628c0..b6f0542e 100644 --- a/modules/core/tag_nohardlinks.py +++ b/modules/core/tag_nohardlinks.py @@ -22,6 +22,8 @@ def __init__(self, qbit_manager): 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 @@ def tag_nohardlinks(self): 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 (" diff --git a/modules/core/tags.py b/modules/core/tags.py index 7eaf2436..8d72c582 100644 --- a/modules/core/tags.py +++ b/modules/core/tags.py @@ -10,6 +10,7 @@ def __init__(self, qbit_manager): 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 @@ def __init__(self, qbit_manager): 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] diff --git a/modules/logs.py b/modules/logs.py index d634c2ec..c41a72e3 100755 --- a/modules/logs.py +++ b/modules/logs.py @@ -1,4 +1,5 @@ """Logging module""" + import io import logging import os diff --git a/modules/qbittorrent.py b/modules/qbittorrent.py index 5b96ce11..7ff97480 100755 --- a/modules/qbittorrent.py +++ b/modules/qbittorrent.py @@ -1,4 +1,5 @@ """Qbittorrent Module""" + import os import sys @@ -10,8 +11,8 @@ 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 @@ def __init__(self, config, params): 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 @@ def __init__(self, config, params): 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 @@ def get_torrent_info(self): 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 @@ def get_torrent_info(self): 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 @@ def get_torrent_info(self): "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 @@ def get_category(self, path): 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: diff --git a/modules/util.py b/modules/util.py index 86468d2c..7a6784e8 100755 --- a/modules/util.py +++ b/modules/util.py @@ -1,4 +1,5 @@ """ Utility functions for qBit Manage. """ + import json import logging import os @@ -276,7 +277,7 @@ def check_for_attribute( 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] diff --git a/modules/webhooks.py b/modules/webhooks.py index 41afd547..bda738a0 100755 --- a/modules/webhooks.py +++ b/modules/webhooks.py @@ -1,4 +1,5 @@ """Class to handle webhooks.""" + import time from json import JSONDecodeError diff --git a/qbit_manage.py b/qbit_manage.py index 0cf24da1..0817abec 100755 --- a/qbit_manage.py +++ b/qbit_manage.py @@ -1,4 +1,4 @@ -#!/usr/bin/python3 +#!/usr/bin/env python3 """qBittorrent Manager.""" import argparse import glob @@ -11,6 +11,7 @@ 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 @@ def get_arg(env_str, default, arg_bool=False, arg_int=False): 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): diff --git a/requirements-dev.txt b/requirements-dev.txt index 05a1e6be..12b9954b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,2 +1,2 @@ flake8==7.0.0 -pre-commit==3.6.0 +pre-commit==3.6.2 diff --git a/requirements.txt b/requirements.txt index 6e5d61c5..ef1258b1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/scripts/delete_torrents_on_low_disk_space.py b/scripts/delete_torrents_on_low_disk_space.py index 463cae77..3b6f8758 100755 --- a/scripts/delete_torrents_on_low_disk_space.py +++ b/scripts/delete_torrents_on_low_disk_space.py @@ -4,13 +4,13 @@ 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": "???"} diff --git a/scripts/edit_tracker.py b/scripts/edit_tracker.py index 064f7b03..63af9f06 100644 --- a/scripts/edit_tracker.py +++ b/scripts/edit_tracker.py @@ -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 @@ # --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) diff --git a/scripts/mover.py b/scripts/mover.py index 116c01fa..edaf51b7 100755 --- a/scripts/mover.py +++ b/scripts/mover.py @@ -12,28 +12,45 @@ 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 @@ def stop_start_torrents(torrent_list, pause=True): 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") diff --git a/scripts/pre-commit/increase_version.sh b/scripts/pre-commit/increase_version.sh index 5ce7754d..11fa945a 100755 --- a/scripts/pre-commit/increase_version.sh +++ b/scripts/pre-commit/increase_version.sh @@ -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=$(/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" diff --git a/setup.py b/setup.py index 127dfe51..26f44488 100755 --- a/setup.py +++ b/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 @@ 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 diff --git a/tox.ini b/tox.ini index 7359ccfd..25fd74c8 100755 --- a/tox.ini +++ b/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"