From 0b285097c95536f6864e0cdd21f33023150e30cf Mon Sep 17 00:00:00 2001 From: Benjamin Harder Date: Tue, 20 Aug 2024 23:28:20 +0200 Subject: [PATCH] New feature: Skipping files that have less than 100% availabiltiy --- .vscode/settings.json | 5 ++- README.md | 12 +++++++ config/config.conf-Example | 1 + config/definitions.py | 1 + src/decluttarr.py | 13 +++++++- src/jobs/skip_unavailable_files.py | 50 ++++++++++++++++++++++++++++++ src/utils/loadScripts.py | 16 ++++++++++ 7 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 src/jobs/skip_unavailable_files.py diff --git a/.vscode/settings.json b/.vscode/settings.json index fe4b83c..275db7b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,7 @@ { "editor.formatOnSave": true, - "editor.defaultFormatter": "ms-python.black-formatter" + "editor.defaultFormatter": "ms-python.black-formatter", + "[python]": { + "editor.defaultFormatter": "mikoz.black-py" + } } \ No newline at end of file diff --git a/README.md b/README.md index 3011642..880345b 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ Feature overview: - Automatically delete slow downloads, after they have been found to be slow multiple times in a row (& trigger download from another source) - Automatically delete downloads belonging to radarr/sonarr/etc. items that are unmonitored - Automatically delete downloads that failed importing since they are not a format upgrade (i.e. a better version is already present) +- Automatically set file to not download if they are not 100% available (missing peers) You may run this locally by launching main.py, or by pulling the docker image. You can find a sample docker-compose.yml [here](#method-1-docker). @@ -84,6 +85,7 @@ services: REMOVE_SLOW: True REMOVE_STALLED: True REMOVE_UNMONITORED: True + SKIP_UNAVAILABLE_FILES: True RUN_PERIODIC_RESCANS: ' { "SONARR": {"MISSING": true, "CUTOFF_UNMET": true, "MAX_CONCURRENT_SCANS": 3, "MIN_DAYS_BEFORE_RESCAN": 7}, @@ -259,6 +261,16 @@ Steers which type of cleaning is applied to the downloads queue - Permissible Values: True, False - Is Mandatory: No (Defaults to False) +**SKIP_UNAVAILABLE_FILES** +- Steers whether files within torrents are marked as 'not download' if they have less then 100% availabiltiy +- The torrent is not removed and will complete for the other files +- After import, the *arr app will trigger a search for the files that were not downloaded +- Note that this is only supported when qBittorrent is configured in decluttarr. +- Also note that this will turn on the setting 'Keep unselected files in ".unwanted" folder' in qBittorrent +- Type: Boolean +- Permissible Values: True, False +- Is Mandatory: No (Defaults to False) + **RUN_PERIODIC_RESCANS** - Steers whether searches are automatically triggered for items that are missing or have not yet met the cutoff diff --git a/config/config.conf-Example b/config/config.conf-Example index b3b75f9..4b54a5b 100644 --- a/config/config.conf-Example +++ b/config/config.conf-Example @@ -12,6 +12,7 @@ REMOVE_ORPHANS = True REMOVE_SLOW = True REMOVE_STALLED = True REMOVE_UNMONITORED = True +SKIP_UNAVAILABLE_FILES = True RUN_PERIODIC_RESCANS = {"SONARR": {"MISSING": true, "CUTOFF_UNMET": true, "MAX_CONCURRENT_SCANS": 3, "MIN_DAYS_BEFORE_RESCAN": 7}, "RADARR": {"MISSING": true, "CUTOFF_UNMET": true, "MAX_CONCURRENT_SCANS": 3, "MIN_DAYS_BEFORE_RESCAN": 7}} [feature_settings] diff --git a/config/definitions.py b/config/definitions.py index f483e23..128e159 100644 --- a/config/definitions.py +++ b/config/definitions.py @@ -19,6 +19,7 @@ REMOVE_SLOW = get_config_value('REMOVE_SLOW', 'features', False, bool, False) REMOVE_STALLED = get_config_value('REMOVE_STALLED', 'features', False, bool, False) REMOVE_UNMONITORED = get_config_value('REMOVE_UNMONITORED', 'features', False, bool, False) +SKIP_UNAVAILABLE_FILES = get_config_value('SKIP_UNAVAILABLE_FILES', 'features', False, bool, False) RUN_PERIODIC_RESCANS = get_config_value('RUN_PERIODIC_RESCANS', 'features', False, dict, {}) # Feature Settings diff --git a/src/decluttarr.py b/src/decluttarr.py index 3fab741..bb15dc1 100644 --- a/src/decluttarr.py +++ b/src/decluttarr.py @@ -55,7 +55,7 @@ async def queueCleaner( sys.exit() # Cleans up the downloads queue - logger.verbose("Cleaning queue on %s:", NAME) + logger.verbose('Cleaning queue on %s:', NAME) # Refresh queue: try: full_queue = await get_queue(BASE_URL, API_KEY, settingsDict, params={full_queue_param: True}) @@ -66,6 +66,17 @@ async def queueCleaner( deleted_downloads = Deleted_Downloads([]) items_detected = 0 + if settingsDict['SKIP_UNAVAILABLE_FILES']: + await skip_unavailable_files( + settingsDict, + BASE_URL, + API_KEY, + NAME, + protectedDownloadIDs, + privateDowloadIDs, + arr_type + ) + if settingsDict["REMOVE_FAILED"]: items_detected += await remove_failed( settingsDict, diff --git a/src/jobs/skip_unavailable_files.py b/src/jobs/skip_unavailable_files.py new file mode 100644 index 0000000..31b7b68 --- /dev/null +++ b/src/jobs/skip_unavailable_files.py @@ -0,0 +1,50 @@ +from src.utils.shared import (errorDetails, formattedQueueInfo, get_queue, privateTrackerCheck, protectedDownloadCheck, execute_checks, permittedAttemptsCheck, remove_download, qBitOffline) +import sys, os, traceback +import logging, verboselogs +logger = verboselogs.VerboseLogger(__name__) +from src.utils.rest import rest_get, rest_post + + +async def skip_unavailable_files(settingsDict, BASE_URL, API_KEY, NAME, protectedDownloadIDs, privateDowloadIDs, arr_type): + # Checks if downloads have less than 100% availability and marks the underyling files that cause it as 'do not download' + # Only works in qbit + try: + failType = '>100% availability' + queue = await get_queue(BASE_URL, API_KEY) + logger.debug('skip_unavailable_files/queue IN: %s', formattedQueueInfo(queue)) + if not queue: return 0 + if await qBitOffline(settingsDict, failType, NAME): return + # Find items affected + + qbitHashes = list(set(queueItem['downloadId'].upper() for queueItem in queue['records'])) + + # Remove private and protected trackers + if settingsDict['IGNORE_PRIVATE_TRACKERS']: + for qbitHash in reversed(qbitHashes): + if qbitHash in privateDowloadIDs: + qbitHashes.remove(qbitHash) + + if settingsDict['IGNORE_PRIVATE_TRACKERS']: + for qbitHash in reversed(qbitHashes): + if qbitHash in privateDowloadIDs: + qbitHashes.remove(qbitHash) + + qbitItems = await rest_get(settingsDict['QBITTORRENT_URL']+'/torrents/info',params={'hashes': ('|').join(qbitHashes)}, cookies=settingsDict['QBIT_COOKIE']) + + for qbitItem in qbitItems: + if 'state' in qbitItem and 'availability' in qbitItem: + if qbitItem['state'] == 'downloading' and qbitItem['availability'] < 1: + logger.info('>>> Detected %s: %s', failType, qbitItem['name']) + logger.verbose('>>>>> Marking following files to "not download":') + qbitItemFiles = await rest_get(settingsDict['QBITTORRENT_URL']+'/torrents/files',params={'hash': qbitItem['hash']}, cookies=settingsDict['QBIT_COOKIE']) + for qbitItemFile in qbitItemFiles: + if all(key in qbitItemFile for key in ['availability', 'progress', 'priority', 'index', 'name']): + if qbitItemFile['availability'] < 1 and qbitItemFile['progress'] < 1 and qbitItemFile['priority'] != 0: + logger.verbose('>>>>> %s', qbitItemFile['name'].split('/')[-1]) + if not settingsDict['TEST_RUN']: + await rest_post(url=settingsDict['QBITTORRENT_URL']+'/torrents/filePrio', data={'hash': qbitItem['hash'].lower(), 'id': qbitItemFile['index'], 'priority': 0}, cookies=settingsDict['QBIT_COOKIE']) + + except Exception as error: + errorDetails(NAME, error) + return + diff --git a/src/utils/loadScripts.py b/src/utils/loadScripts.py index 2300049..32f1be9 100644 --- a/src/utils/loadScripts.py +++ b/src/utils/loadScripts.py @@ -95,6 +95,8 @@ def showSettings(settingsDict): logger.info('%s | Removing slow downloads (%s)', str(settingsDict['REMOVE_SLOW']), 'REMOVE_SLOW') logger.info('%s | Removing stalled downloads (%s)', str(settingsDict['REMOVE_STALLED']), 'REMOVE_STALLED') logger.info('%s | Removing downloads belonging to unmonitored items (%s)', str(settingsDict['REMOVE_UNMONITORED']), 'REMOVE_UNMONITORED') + logger.info('%s | Skipping files with <100%% availability (%s)', str(settingsDict['SKIP_UNAVAILABLE_FILES']), 'SKIP_UNAVAILABLE_FILES') + for arr_type, RESCAN_SETTINGS in settingsDict['RUN_PERIODIC_RESCANS'].items(): logger.info('%s/%s (%s) | Search missing/cutoff-unmet items. Max queries/list: %s. Min. days to re-search: %s (%s)', RESCAN_SETTINGS['MISSING'], RESCAN_SETTINGS['CUTOFF_UNMET'], arr_type, RESCAN_SETTINGS['MAX_CONCURRENT_SCANS'], RESCAN_SETTINGS['MIN_DAYS_BEFORE_RESCAN'], 'RUN_PERIODIC_RESCANS') logger.info('') @@ -229,6 +231,20 @@ async def createQbitProtectionTag(settingsDict): if not settingsDict['TEST_RUN']: await rest_post(url=settingsDict['QBITTORRENT_URL']+'/torrents/createTags', data={'tags': settingsDict['NO_STALLED_REMOVAL_QBIT_TAG']}, headers={'content-type': 'application/x-www-form-urlencoded'}, cookies=settingsDict['QBIT_COOKIE']) +async def setQbitUnwantedFolder(settingsDict): + # Creates the qBit Protection tag if not already present + if settingsDict['QBITTORRENT_URL']: + if settingsDict['SKIP_UNAVAILABLE_FILES']: + qBit_settings = await rest_get(settingsDict['QBITTORRENT_URL']+'/app/preferences',cookies=settingsDict['QBIT_COOKIE']) + if not qBit_settings['use_unwanted_folder']: + logger.info('Enabling the qBit setting \'Keep unselect files in ".unwanted" folder\'') + + if not settingsDict['NO_STALLED_REMOVAL_QBIT_TAG'] in current_tags: + if settingsDict['QBITTORRENT_URL']: + logger.info('Creating tag in qBittorrent: %s', settingsDict['NO_STALLED_REMOVAL_QBIT_TAG']) + if not settingsDict['TEST_RUN']: + await rest_post(url=settingsDict['QBITTORRENT_URL']+'/appp/setPreferences', data={'use_unwanted_folder': True}, headers={'content-type': 'application/x-www-form-urlencoded'}, cookies=settingsDict['QBIT_COOKIE']) + exit() def showLoggerLevel(settingsDict): logger.info('#' * 50) if settingsDict['LOG_LEVEL'] == 'INFO':