Skip to content

Commit

Permalink
New feature: Skipping files that have less than 100% availabiltiy
Browse files Browse the repository at this point in the history
  • Loading branch information
ManiMatter committed Jan 11, 2025
1 parent 2041337 commit 0b28509
Show file tree
Hide file tree
Showing 7 changed files with 96 additions and 2 deletions.
5 changes: 4 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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},
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions config/config.conf-Example
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
1 change: 1 addition & 0 deletions config/definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 12 additions & 1 deletion src/decluttarr.py
Original file line number Diff line number Diff line change
Expand Up @@ -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})
Expand All @@ -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,
Expand Down
50 changes: 50 additions & 0 deletions src/jobs/skip_unavailable_files.py
Original file line number Diff line number Diff line change
@@ -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

16 changes: 16 additions & 0 deletions src/utils/loadScripts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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('')
Expand Down Expand Up @@ -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':
Expand Down

0 comments on commit 0b28509

Please sign in to comment.