Add torrent post-import category action (#1148)

## Summary

- add a **Change Category** torrent completion action and conditionally
show its post-import category/label setting
- update kept torrents only after a successful library import
- support qBittorrent categories, Transmission labels, Deluge's Label
plugin, and rTorrent's `custom1` label
- preserve existing Keep/Remove behavior and document the new
environment setting

## Behavior

Category changes happen from `post_process_cleanup`, after output
transfer and post-processing complete. This keeps the existing hardlink
flow unchanged. An empty post-import category is a no-op, and client API
failures are logged without turning a successful library import into a
failure.

## Validation

- `pytest -n 0 tests/prowlarr/test_qbittorrent_client.py
tests/prowlarr/test_transmission_client.py
tests/prowlarr/test_deluge_client.py
tests/prowlarr/test_rtorrent_client.py tests/prowlarr/test_handler.py
tests/config/test_generate_env_docs.py` — 161 passed
- `ruff check` on all changed Python files — passed
- `basedpyright` on the changed client implementation files — passed
- full `basedpyright shelfmark/download/clients` currently reports two
pre-existing errors in the new debrid connection-test code at
`settings.py:546` and `settings.py:563`, outside this PR's diff
- multi-architecture Docker images built successfully for `linux/amd64`
and `linux/arm64`
This commit is contained in:
Tilian B
2026-07-30 23:38:53 -04:00
committed by GitHub
parent 340853477f
commit ff770940ca
13 changed files with 233 additions and 9 deletions
+55
View File
@@ -1456,6 +1456,61 @@ class TestProwlarrHandlerFileStaging:
class TestProwlarrHandlerPostProcessCleanup:
def test_torrent_change_category_sets_post_import_category(self):
handler = ProwlarrHandler()
task = DownloadTask(task_id="torrent-category", source="prowlarr", title="Test")
mock_client = MagicMock()
mock_client.name = "qbittorrent"
mock_client.set_category.return_value = True
handler._cleanup_refs[task.task_id] = (mock_client, "abc123", "torrent")
config_values = {
"PROWLARR_TORRENT_ACTION": "change_category",
"PROWLARR_TORRENT_POST_IMPORT_CATEGORY": "imported",
}
with patch(
"shelfmark.download.clients.base_handler.config.get",
side_effect=lambda key, default="": config_values.get(key, default),
):
handler.post_process_cleanup(task, success=True)
mock_client.set_category.assert_called_once_with("abc123", "imported")
mock_client.remove.assert_not_called()
def test_torrent_keep_does_not_change_category(self):
handler = ProwlarrHandler()
task = DownloadTask(task_id="torrent-no-category", source="prowlarr", title="Test")
mock_client = MagicMock()
handler._cleanup_refs[task.task_id] = (mock_client, "abc123", "torrent")
with patch("shelfmark.download.clients.base_handler.config.get", return_value="keep"):
handler.post_process_cleanup(task, success=True)
mock_client.set_category.assert_not_called()
mock_client.remove.assert_not_called()
def test_torrent_change_category_ignores_empty_category(self):
handler = ProwlarrHandler()
task = DownloadTask(task_id="torrent-empty-category", source="prowlarr", title="Test")
mock_client = MagicMock()
handler._cleanup_refs[task.task_id] = (mock_client, "abc123", "torrent")
config_values = {
"PROWLARR_TORRENT_ACTION": "change_category",
"PROWLARR_TORRENT_POST_IMPORT_CATEGORY": "",
}
with patch(
"shelfmark.download.clients.base_handler.config.get",
side_effect=lambda key, default="": config_values.get(key, default),
):
handler.post_process_cleanup(task, success=True)
mock_client.set_category.assert_not_called()
mock_client.remove.assert_not_called()
def test_usenet_move_triggers_client_cleanup(self):
handler = ProwlarrHandler()
task = DownloadTask(task_id="cleanup-test", source="prowlarr", title="Test")