Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Add option to set 'obsolete'-tag to private torrents that are not needed but kept anyway. #194

Open
wants to merge 6 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ You can find a sample docker-compose.yml [here](#method-1-docker).
- When detecting slow downloads, the speeds provided by the \*arr apps will be used, which is less accurate than what qBittorrent returns when queried directly
- The feature that allows to protect downloads from removal (NO_STALLED_REMOVAL_QBIT_TAG) does not work
- The feature that ignores private trackers does not work
- The "obsolete" feature that tags private torrents due for removal rather than removing them does not work
- If you see strange errors such as "found 10 / 3 times", consider turning on the setting "Reject Blocklisted Torrent Hashes While Grabbing". On nightly Radarr/Sonarr/Readarr/Lidarr/Whisparr, the option is located under settings/indexers in the advanced options of each indexer, on Prowlarr it is under settings/apps and then the advanced settings of the respective app
- When broken torrents are removed the files belonging to them are deleted
- Across all removal types: A new download from another source is automatically added by radarr/sonarr/lidarr/readarr/whisparr (if available)
Expand Down Expand Up @@ -74,7 +75,7 @@ services:
# SSL_VERIFICATION: False
LOG_LEVEL: INFO

## Features
## Features
REMOVE_TIMER: 10
REMOVE_FAILED: True
REMOVE_FAILED_IMPORTS: True
Expand All @@ -84,6 +85,7 @@ services:
REMOVE_SLOW: True
REMOVE_STALLED: True
REMOVE_UNMONITORED: True
SET_OBSOLETE_QBIT_TAG: True
RUN_PERIODIC_RESCANS: '
{
"SONARR": {"MISSING": true, "CUTOFF_UNMET": true, "MAX_CONCURRENT_SCANS": 3, "MIN_DAYS_BEFORE_RESCAN": 7},
Expand All @@ -93,7 +95,8 @@ services:
# Feature Settings
PERMITTED_ATTEMPTS: 3
NO_STALLED_REMOVAL_QBIT_TAG: Don't Kill
MIN_DOWNLOAD_SPEED: 100
OBSOLETE_QBIT_TAG: Obsolete
MIN_DOWNLOAD_SPEED: 100
FAILED_IMPORT_MESSAGE_PATTERNS: '
[
"Not a Custom Format upgrade for existing",
Expand Down Expand Up @@ -259,6 +262,15 @@ Steers which type of cleaning is applied to the downloads queue
- Permissible Values: True, False
- Is Mandatory: No (Defaults to False)

**SET_OBSOLETE_QBIT_TAG**
- Set a tag on torrents in qBittorrent that can be removed, but are kept because they are private torrents.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • If this feature is turned on, private torrents in qBittorrent are not removed once the conditions for removal are met; instead, they are tagged with the obsolete-tag

I would formulate it like this - if you are OK

- Note: Has no effect when `IGNORE_PRIVATE_TRACKERS==False`.
- The tag can be used by third-party tools to remove these torrents after required seeding time has passed.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tag can be used by third-party tools (such as qbit_manage) to remove these torrents after required seeding time has passed.

would it make sense to add qbit_manage as example?

- Tag is automatically created in qBittorrent (required qBittorrent is reachable on `QBITTORRENT_URL`)
- 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 Expand Up @@ -317,6 +329,13 @@ If it you face issues, please first check the closed issues before opening a new
- Type: String
- Is Mandatory: No (Defaults to `Don't Kill`)

**OBSOLETE_QBIT_TAG**
- Downloads in qBittorrent will receive this tag when: (1) `SET_OBSOLETE_QBIT_TAG==True`, (2) `IGNORE_PRIVATE_TRACKERS==True`, (3) torrent is private, (4) torrent is due for removal.
- Note: the tag can be used by third-party tools to remove these torrents after required seeding time has passed.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: the tag can be used by third-party tools (such as qbit_manage)to remove these torrents after required seeding time has passed.

would it make sense to add qbit_manage as example?

- Tag is automatically created in qBittorrent (required qBittorrent is reachable on `QBITTORRENT_URL`)
- Type: String
- Is Mandatory: No (Defaults to `Obsolete`)

**IGNORE_PRIVATE_TRACKERS**

- Private torrents in qBittorrent will not be removed from the queue if this is set to true
Expand Down
2 changes: 2 additions & 0 deletions config/config.conf-Example
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ REMOVE_ORPHANS = True
REMOVE_SLOW = True
REMOVE_STALLED = True
REMOVE_UNMONITORED = True
SET_OBSOLETE_QBIT_TAG = 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]
MIN_DOWNLOAD_SPEED = 100
PERMITTED_ATTEMPTS = 3
NO_STALLED_REMOVAL_QBIT_TAG = Don't Kill
OBSOLETE_QBIT_TAG = Obsolete
IGNORE_PRIVATE_TRACKERS = FALSE
FAILED_IMPORT_MESSAGE_PATTERNS = ["Not a Custom Format upgrade for existing", "Not an upgrade for existing"]
IGNORED_DOWNLOAD_CLIENTS = ["emulerr"]
Expand Down
2 changes: 2 additions & 0 deletions config/definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@
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)
SET_OBSOLETE_QBIT_TAG = get_config_value('SET_OBSOLETE_QBIT_TAG', 'features', False, bool, False)
RUN_PERIODIC_RESCANS = get_config_value('RUN_PERIODIC_RESCANS', 'features', False, dict, {})

# Feature Settings
MIN_DOWNLOAD_SPEED = get_config_value('MIN_DOWNLOAD_SPEED', 'feature_settings', False, int, 0)
PERMITTED_ATTEMPTS = get_config_value('PERMITTED_ATTEMPTS', 'feature_settings', False, int, 3)
NO_STALLED_REMOVAL_QBIT_TAG = get_config_value('NO_STALLED_REMOVAL_QBIT_TAG', 'feature_settings', False, str, 'Don\'t Kill')
OBSOLETE_QBIT_TAG = get_config_value('OBSOLETE_QBIT_TAG', 'feature_settings', False, str, 'Obsolete')
IGNORE_PRIVATE_TRACKERS = get_config_value('IGNORE_PRIVATE_TRACKERS', 'feature_settings', False, bool, True)
FAILED_IMPORT_MESSAGE_PATTERNS = get_config_value('FAILED_IMPORT_MESSAGE_PATTERNS','feature_settings', False, list, [])
IGNORED_DOWNLOAD_CLIENTS = get_config_value('IGNORED_DOWNLOAD_CLIENTS', 'feature_settings', False, list, [])
Expand Down
3 changes: 3 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ async def main(settingsDict):
# Create qBit protection tag if not existing
await createQbitProtectionTag(settingsDict)

# Create qBit obsolete tag if not existing
await createQbitObsoleteTag(settingsDict)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3. The tag should only be created if the feature is being used in qbit

My point was referring to this line. In my mind you should only create a obsolete tag in qbit if this feature is even used.

if settingsDict['SET_OBSOLETE_QBIT_TAG']
     await createQbitObsoleteTag(settingsDict)


# Show Logger Level
showLoggerLevel(settingsDict)

Expand Down
17 changes: 15 additions & 2 deletions src/utils/loadScripts.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ 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 | Setting obsolete tag on private torrents that are due for removal (%s)', str(settingsDict['SET_OBSOLETE_QBIT_TAG']), 'SET_OBSOLETE_QBIT_TAG')
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logger.info("%s | Keeping private torrents in qBit due for removal and tagging instead (tag: '%s')", str(settingsDict['SET_OBSOLETE_QBIT_TAG']), 'SET_OBSOLETE_QBIT_TAG')

Proposal above.

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 All @@ -101,8 +102,10 @@ def showSettings(settingsDict):
logger.info('Minimum speed enforced: %s KB/s', str(settingsDict['MIN_DOWNLOAD_SPEED']))
logger.info('Permitted number of times before stalled/missing metadata/slow downloads are removed: %s', str(settingsDict['PERMITTED_ATTEMPTS']))
if settingsDict['QBITTORRENT_URL']:
logger.info('Downloads with this tag will be skipped: \"%s\"', settingsDict['NO_STALLED_REMOVAL_QBIT_TAG'])
logger.info('Private Trackers will be skipped: %s', settingsDict['IGNORE_PRIVATE_TRACKERS'])
logger.info('Downloads with this tag will be skipped: \"%s\"', settingsDict['NO_STALLED_REMOVAL_QBIT_TAG'])
if settingsDict['SET_OBSOLETE_QBIT_TAG'] and settingsDict['OBSOLETE_QBIT_TAG']:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and settingsDict['OBSOLETE_QBIT_TAG']:

isn't that check redundant? this value will always be true, since you are setting a default in the definitions.py?

logger.info('Obsolete private torrents will be given this tag: \"%s\"', settingsDict['OBSOLETE_QBIT_TAG'])
logger.info('Private Trackers will be skipped: %s', settingsDict['IGNORE_PRIVATE_TRACKERS'])
if settingsDict['IGNORED_DOWNLOAD_CLIENTS']:
logger.info('Download clients skipped: %s',", ".join(settingsDict['IGNORED_DOWNLOAD_CLIENTS']))
logger.info('')
Expand Down Expand Up @@ -225,6 +228,16 @@ 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 createQbitObsoleteTag(settingsDict):
# Creates the qBit Obsolete tag if not already present, feature is enabled, and tag is not an empty string
if settingsDict['QBITTORRENT_URL'] and settingsDict['SET_OBSOLETE_QBIT_TAG'] and settingsDict['OBSOLETE_QBIT_TAG']:
current_tags = await rest_get(settingsDict['QBITTORRENT_URL']+'/torrents/tags',cookies=settingsDict['QBIT_COOKIE'])
if not settingsDict['OBSOLETE_QBIT_TAG'] in current_tags:
if settingsDict['QBITTORRENT_URL']:
logger.info('Creating tag in qBittorrent: %s', settingsDict['OBSOLETE_QBIT_TAG'])
if not settingsDict['TEST_RUN']:
await rest_post(url=settingsDict['QBITTORRENT_URL']+'/torrents/createTags', data={'tags': settingsDict['OBSOLETE_QBIT_TAG']}, headers={'content-type': 'application/x-www-form-urlencoded'}, cookies=settingsDict['QBIT_COOKIE'])

def showLoggerLevel(settingsDict):
logger.info('#' * 50)
if settingsDict['LOG_LEVEL'] == 'INFO':
Expand Down
20 changes: 15 additions & 5 deletions src/utils/shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,11 +309,18 @@ async def remove_download(
if removeFromClient:
logger.info(">>> Removing %s download: %s", failType, affectedItem["title"])
else:
logger.info(
">>> Removing %s download (without removing from torrent client): %s",
failType,
affectedItem["title"],
)
if settingsDict['SET_OBSOLETE_QBIT_TAG'] and settingsDict['OBSOLETE_QBIT_TAG']:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

settingsDict['OBSOLETE_QBIT_TAG'] - same as above. isnt this obsolete due to default value?

logger.info(
">>> Removing %s download (without removing from torrent client but with setting obsolete tag): %s",
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

">>> Removing %s download (keeping it in torrent client and adding tag '%s'

where %s is settingsDict['OBSOLETE_QBIT_TAG']

proposal above.

failType,
affectedItem["title"],
)
else:
logger.info(
">>> Removing %s download (without removing from torrent client): %s",
failType,
affectedItem["title"],
)

# Print out detailed removal messages (if any were added in the jobs)
if "removal_messages" in affectedItem:
Expand All @@ -326,6 +333,9 @@ async def remove_download(
API_KEY,
{"removeFromClient": removeFromClient, "blocklist": addToBlocklist},
)
if not removeFromClient and settingsDict['QBITTORRENT_URL'] and settingsDict['SET_OBSOLETE_QBIT_TAG'] and settingsDict['OBSOLETE_QBIT_TAG'] and affectedItem["downloadId"]:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

settingsDict['OBSOLETE_QBIT_TAG'] -> redundant?

await rest_post(url=settingsDict['QBITTORRENT_URL']+'/torrents/createTags', data={'hashes': affectedItem["downloadId"], 'tags': settingsDict['OBSOLETE_QBIT_TAG']}, headers={'content-type': 'application/x-www-form-urlencoded'}, cookies=settingsDict['QBIT_COOKIE'])

deleted_downloads.dict.append(affectedItem["downloadId"])

logger.debug(
Expand Down
21 changes: 21 additions & 0 deletions tests/utils/remove_download/test_remove_download_1.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,24 @@ async def test_schizophrenic_removal_with_removal_messages(monkeypatch, caplog):
monkeypatch=monkeypatch,
caplog=caplog,
)

@pytest.mark.asyncio
async def test_obsolete_removal_with_removal_messages(monkeypatch, caplog):
settingsDict = {"TEST_RUN": True, "SET_OBSOLETE_QBIT_TAG": True}
removeFromClient = False
expected_removal_messages = {
">>> Removing failed import download (without removing from torrent client but with setting obsolete tag): Sonarr Title 1",
">>>>> Tracked Download State: importBlocked",
">>>>> Status Messages (matching specified patterns):",
">>>>> - Episode XYZ was not found in the grabbed release: Sonarr Title 2.mkv",
">>>>> - And yet another message",
}
await run_test(
settingsDict=settingsDict,
expected_removal_messages=expected_removal_messages,
failType=failType,
removeFromClient=removeFromClient,
mock_data_file=mock_data_file,
monkeypatch=monkeypatch,
caplog=caplog,
)
Loading