Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
b433e06
feat: add remove_completed job and new download client job type
Baz00k Sep 11, 2025
42d6d61
docs: update README to include details about REMOVE_COMPLETED job
Baz00k Sep 11, 2025
407ca1b
fix: skipped for usenet test attribute error
Baz00k Sep 11, 2025
c96accc
fix: loading remove_completed job settings from env
Baz00k Sep 11, 2025
c474fc9
Merge branch 'dev' into dev
Baz00k Sep 14, 2025
4df383d
Merge branch 'dev' into dev
Baz00k Sep 24, 2025
17805c5
Merge branch 'dev' into dev
Baz00k Oct 1, 2025
4360499
fix: use global limits as fallback
Baz00k Sep 29, 2025
20bc294
fix: log only if client has any jobs enabled
Baz00k Oct 3, 2025
f9eaa98
refactor: check the client against list of supported clients
Baz00k Oct 3, 2025
eb79c32
chore: add better examples and exmplanation in readme
Baz00k Oct 3, 2025
cc3db3d
chore: revert table of contents change
Baz00k Oct 3, 2025
8b7cbc9
fix: failing test after log update
Baz00k Oct 3, 2025
4a3dfd5
refactor: streamline item removal for download clients
Baz00k Oct 14, 2025
c0ac6dc
refactor: use DOWNLOAD_CLIENT_TYPES for download client iteration
Baz00k Oct 15, 2025
b8db7c1
fix: failing tests after client job base class update
Baz00k Oct 15, 2025
461f974
chore: update remove_completed job docs
Baz00k Oct 15, 2025
efe2e23
chore: improve tests reliablility by not checking log output
Baz00k Oct 15, 2025
7bb509d
Hid additional internal attributes from logging
ManiMatter Oct 19, 2025
da22973
Always putting obsolete into output, even if not used
ManiMatter Oct 19, 2025
8f1c0bb
Fixing filter of internal attributes
ManiMatter Oct 19, 2025
1858c3d
Few refinements, and added obsolete as default target tag
ManiMatter Oct 19, 2025
78ac8d4
Renamed remove_completed -> remove_done_seeding
ManiMatter Oct 19, 2025
2fb373a
pylint fix
ManiMatter Oct 19, 2025
8dab88d
Merge branch 'dev' into dev
ManiMatter Oct 24, 2025
090af35
Merge branch 'dev' into dev
ManiMatter Oct 25, 2025
98ea479
Merge branch 'dev' into dev
ManiMatter Oct 26, 2025
057cbdc
Finalized readme
ManiMatter Nov 1, 2025
b39b8d2
Merge branch 'dev' of https://github.com/Baz00k/decluttarr into pr/Ba…
ManiMatter Nov 1, 2025
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
15 changes: 8 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Looking to **upgrade from V1 to V2**? Look [here](#upgrading-from-v1-to-v2)
- [REMOVE_SLOW](#remove_slow)
- [REMOVE_STALLED](#remove_stalled)
- [REMOVE_UNMONITORED](#remove_unmonitored)
- [REMOVE_COMPLETED](#remove_completed)
- [REMOVE_DONE_SEEDING](#remove_done_seeding)
- [SEARCH_CUTOFF_UNMET](#search_unmet_cutoff)
- [SEARCH_MISSING](#search_missing)
- [DETECT_DELETIONS](#detect_deletions)
Expand Down Expand Up @@ -68,7 +68,7 @@ Feature overview:
- Removing downloads that are repeatedly have been found to be slow (remove_slow)
- Removing downloads that are stalled (remove_stalled)
- Removing downloads belonging to movies/series/albums etc. that have been marked as "unmonitored" (remove_unmonitored)
- Removing completed downloads from your download client that match certain criteria (remove_completed)
- Removing completed downloads from your download client that match certain criteria (remove_done_seeding)
- Periodically searching for better content on movies/series/albums etc. where cutoff has not been reached yet (search_cutoff_unmet)
- Periodically searching for missing content that has not yet been found (search_missing)

Expand Down Expand Up @@ -227,7 +227,7 @@ services:
# As written above, these can also be set as Job Defaults so you don't have to specify them as granular as below.
# REMOVE_BAD_FILES: |
# keep_archives: True
# REMOVE_COMPLETED: |
# REMOVE_DONE_SEEDING: |
# target_tags:
# - "Obsolete"
# target_categories:
Expand Down Expand Up @@ -501,11 +501,12 @@ This is the interesting section. It defines which job you want decluttarr to run
- This may be helpful if you use a tool such as [unpackerr](https://github.com/Unpackerr/unpackerr) that can handle it
- However, you may also find that these packages may contain bad/malicious files (which will not removed by decluttarr)

#### REMOVE_COMPLETED
#### REMOVE_DONE_SEEDING

- Removes completed downloads from the download client's queue when they meet your selection criteria (tags and/or categories).
- What "completed" means:
- Downloads are considered completed when the seeding goals configured in your download client are met: either the ratio limit or the seeding time limit (per-torrent overrides or the global limits).
- Removes downloads that are completed and are done with seeding from the download client's queue when they meet your selection criteria (tags and/or categories).
- "Done Seeding" means the seeding goals configured in your download client are met:
- Either the ratio limit is reached
- Or the seeding time limit (per-torrent overrides or the global limits).
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This line suggests that per-torrent overrides only apply to the seeding time and not to ratio

Copy link
Owner

Choose a reason for hiding this comment

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

Would this be more accurate?

  • "Done Seeding" means that the Ratio limit or Seeding Time limit for your download has been reached
  • The limits are taken from your global settings in your download client, or the download-specific overrides

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, looks great

- Type: Boolean or Dict
- Permissible Values:
- If Bool: True, False
Expand Down
2 changes: 1 addition & 1 deletion config/config_example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ job_defaults:
jobs:
remove_bad_files:
# keep_archives: true
remove_completed:
remove_done_seeding:
# target_tags:
# - "Obsolete"
# target_categories:
Expand Down
4 changes: 2 additions & 2 deletions src/job_manager.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Cleans the download queue
from src.jobs.remove_bad_files import RemoveBadFiles
from src.jobs.remove_completed import RemoveCompleted
from src.jobs.remove_done_seeding import RemoveDoneSeeding
from src.jobs.remove_failed_downloads import RemoveFailedDownloads
from src.jobs.remove_failed_imports import RemoveFailedImports
from src.jobs.remove_metadata_missing import RemoveMetadataMissing
Expand Down Expand Up @@ -176,7 +176,7 @@ def _get_download_client_jobs_for_client(self, client, client_type):
Each job is included if the corresponding attribute exists and is truthy in settings.jobs.
"""
download_client_job_classes = {
"remove_completed": RemoveCompleted,
"remove_done_seeding": RemoveDoneSeeding,
}

jobs = []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@
]


class RemoveCompleted(DownloadClientRemovalJob):
class RemoveDoneSeeding(DownloadClientRemovalJob):
"""Job to remove completed torrents that match specific tags or categories."""

SUPPORTED_CLIENTS: ClassVar[list[str]] = ["qbittorrent"]

async def run(self) -> int:
if self.download_client_type not in self.SUPPORTED_CLIENTS:
logger.debug(
f"remove_completed.py/run: Skipping job '{self.job_name}' for unsupported client {self.download_client.name}.",
f"remove_done_seeding.py/run: Skipping job '{self.job_name}' for unsupported client {self.download_client.name}.",
)
return 0

Expand All @@ -34,7 +34,7 @@ async def _get_items_to_remove(self, items: list) -> list:

if not target_tags and not target_categories:
logger.debug(
"remove_completed.py/_get_items_to_remove: No target tags or categories specified for remove_completed job.",
"remove_done_seeding.py/_get_items_to_remove: No target tags or categories specified for remove_done_seeding job.",
)
return []

Expand All @@ -47,7 +47,7 @@ async def _get_items_to_remove(self, items: list) -> list:

for item in items_to_remove:
logger.debug(
f"remove_completed.py/_get_items_to_remove: Found completed item to remove: {item.get('name', 'unknown')}",
f"remove_done_seeding.py/_get_items_to_remove: Found completed item to remove: {item.get('name', 'unknown')}",
)

return items_to_remove
Expand Down
2 changes: 1 addition & 1 deletion src/settings/_jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def __init__(self, config, settings):

def _set_job_defaults(self):
self.remove_bad_files = JobParams(keep_archives=self.job_defaults.keep_archives)
self.remove_completed = JobParams(target_tags=self.job_defaults.target_tags)
self.remove_done_seeding = JobParams(target_tags=self.job_defaults.target_tags)
self.remove_failed_downloads = JobParams()
self.remove_failed_imports = JobParams(
message_patterns=self.job_defaults.message_patterns,
Expand Down
2 changes: 1 addition & 1 deletion src/settings/_user_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
],
"jobs": [
"REMOVE_BAD_FILES",
"REMOVE_COMPLETED",
"REMOVE_DONE_SEEDING",
"REMOVE_FAILED_DOWNLOADS",
"REMOVE_FAILED_IMPORTS",
"REMOVE_METADATA_MISSING",
Expand Down
34 changes: 18 additions & 16 deletions tests/jobs/test_remove_completed.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
"""Tests for the remove_completed job."""
"""Tests for the remove_done_seeding job."""

from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from src.jobs.remove_completed import COMPLETED_STATES, RemoveCompleted
from src.jobs.remove_done_seeding import COMPLETED_STATES, RemoveDoneSeeding


def create_mock_settings(target_tags=None, target_categories=None):
"""Create mock settings for testing."""
settings = MagicMock()
settings.jobs = MagicMock()
settings.jobs.remove_completed.enabled = True
settings.jobs.remove_completed.target_tags = target_tags or []
settings.jobs.remove_completed.target_categories = target_categories or []
settings.jobs.remove_done_seeding.enabled = True
settings.jobs.remove_done_seeding.target_tags = target_tags or []
settings.jobs.remove_done_seeding.target_categories = target_categories or []
settings.general = MagicMock()
settings.general.protected_tag = "protected"
return settings
Expand Down Expand Up @@ -119,19 +119,19 @@ def create_mock_download_client(items, client_name="mock_client_name"):
),
],
)
async def test_remove_completed_logic(
async def test_remove_done_seeding_logic(
item_properties: dict,
target_tags: list,
target_categories: list,
should_be_removed: bool,
):
"""Test the logic of the remove_completed job with various scenarios."""
"""Test the logic of the remove_done_seeding job with various scenarios."""
item = {**ITEM_DEFAULTS, **item_properties, "name": "test_item"}

settings = create_mock_settings(target_tags, target_categories)
client = create_mock_download_client([item])

job = RemoveCompleted(client, "qbittorrent", settings, "remove_completed")
job = RemoveDoneSeeding(client, "qbittorrent", settings, "remove_done_seeding")

items_to_remove = await job._get_items_to_remove(await client.get_qbit_items())

Expand All @@ -143,11 +143,11 @@ async def test_remove_completed_logic(


@pytest.mark.asyncio
async def test_remove_completed_skipped_for_sabnzbd():
"""Test that the remove_completed job is skipped for SABnzbd clients."""
async def test_remove_done_seeding_skipped_for_sabnzbd():
"""Test that the remove_done_seeding job is skipped for SABnzbd clients."""
settings = create_mock_settings()
client = create_mock_download_client([], client_name="mock_client_name")
job = RemoveCompleted(client, "sabnzbd", settings, "remove_completed")
job = RemoveDoneSeeding(client, "sabnzbd", settings, "remove_done_seeding")

# Test that the job returns 0 for unsupported clients
result = await job.run()
Expand All @@ -158,7 +158,7 @@ async def test_remove_completed_skipped_for_sabnzbd():


@pytest.mark.asyncio
async def test_remove_completed_test_run_enabled():
async def test_remove_done_seeding_test_run_enabled():
"""Test that no items are actually removed when test_run is enabled."""
item = {
**ITEM_DEFAULTS,
Expand All @@ -170,7 +170,7 @@ async def test_remove_completed_test_run_enabled():
settings = create_mock_settings(target_tags=["tag1"])
settings.general.test_run = True
client = create_mock_download_client([item])
job = RemoveCompleted(client, "qbittorrent", settings, "remove_completed")
job = RemoveDoneSeeding(client, "qbittorrent", settings, "remove_done_seeding")

with patch.object(
client,
Expand All @@ -188,7 +188,7 @@ async def test_remove_completed_test_run_enabled():

@pytest.mark.asyncio
@pytest.mark.parametrize("protected_on", ["tag", "category"])
async def test_remove_completed_with_protected_item(protected_on):
async def test_remove_done_seeding_with_protected_item(protected_on):
"""Test that items with a protected tag or category are not removed."""
item_properties = {"ratio": 2, "ratio_limit": 2, "name": "protected_item"}
target_tags = ["tag1"]
Expand All @@ -209,7 +209,7 @@ async def test_remove_completed_with_protected_item(protected_on):
target_categories=target_categories,
)
client = create_mock_download_client([item])
job = RemoveCompleted(client, "qbittorrent", settings, "remove_completed")
job = RemoveDoneSeeding(client, "qbittorrent", settings, "remove_done_seeding")

with patch.object(
job,
Expand All @@ -224,7 +224,9 @@ async def test_remove_completed_with_protected_item(protected_on):
@pytest.mark.asyncio
async def test_is_completed_logic():
"""Test the internal _is_completed logic with different states and limits."""
job = RemoveCompleted(MagicMock(), "qbittorrent", MagicMock(), "remove_completed")
job = RemoveDoneSeeding(
MagicMock(), "qbittorrent", MagicMock(), "remove_done_seeding"
)

# Completed states
for state in COMPLETED_STATES:
Expand Down