Files
shelfmark/tests/download/test_torrent_ledger.py
T
splitsec2 bb13fae0bb feat(download): sweep dead torrents and Soulseek leftovers on their own timer (#1619)
This adds two cleanups that nobody notices until they matter, and puts
them on their own timer. Both do nothing until their setting and client
are configured.

The dead-torrent sweep removes a torrent Shelfmark added once it has
been stalled, or stuck fetching metadata, for `STALE_TORRENT_HOURS` with
no running download owning it, and its partial data goes with it. It
only touches torrents this instance added, tracked in a ledger of
hashes, whatever category they are in, and only on qBittorrent. The
default is 6 hours (3 to 72, and 0 turns it off). I am not sure 6 is
right for everyone, so say if you would rather ship it off.

The Soulseek sweep covers what a restart leaves in slskd's downloads
folder. A finished folder nothing imported gets imported, a partial
folder is removed after `SLSKD_CLEAN_LEFTOVERS_DAYS`, and unfinished
transfers no running download owns are picked up again when their
request still waits for them.

Both run from a small background thread every 15 minutes, after a 30
second startup delay. I first ran them from the Hardcover scheduler, but
that only runs when Hardcover sync or auto-download is on, so anyone
using neither would never get the cleanup. A separate thread keeps them
independent, and the interval is fixed so there is no new setting.

The new tests mock the sweeps. I also started a scratch instance, and
the thread starts and the first cycle is a no-op when nothing is
configured. The timer has not run against a real qBittorrent or slskd,
though the two sweeps themselves have been in my fork's builds since
October. Full Python suite, ruff and basedpyright pass.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **New Features**
* Shelfmark periodically recovers eligible complete Soulseek downloads
and reattaches unfinished transfers. It can also clean up eligible old
folders while protecting active, wanted, and already-imported downloads.
* Shelfmark can remove tracked qBittorrent torrents that have stalled or
exceeded the configured inactivity limit, while preserving torrents
associated with active tasks. Set the limit to 0 to disable cleanup.
* **Bug Fixes**
* Isolated Soulseek downloads reject task IDs that could produce unsafe
or mismatched destination folders.
* **Documentation**
* Added configuration guidance for Soulseek recovery and cleanup, and
qBittorrent stale-torrent cleanup, including defaults and eligibility
rules.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-11-09 13:20:21 -06:00

56 lines
1.5 KiB
Python

"""The ledger of torrents this instance added."""
from __future__ import annotations
import pytest
from shelfmark.download import torrent_ledger
@pytest.fixture(autouse=True)
def _ledger_file(tmp_path, monkeypatch):
monkeypatch.setattr(torrent_ledger, "_path", lambda: tmp_path / "torrent_ledger.db")
def test_a_recorded_hash_is_kept_lower_case_with_its_task():
torrent_ledger.record("ABCDEF", "task-1")
assert torrent_ledger.entries() == {"abcdef": "task-1"}
def test_recording_a_hash_again_moves_it_to_the_new_task():
torrent_ledger.record("abc", "task-1")
torrent_ledger.record("ABC", "task-2")
assert torrent_ledger.entries() == {"abc": "task-2"}
def test_claiming_moves_a_known_hash_and_never_adds_an_unknown_one():
torrent_ledger.record("abc", "task-1")
torrent_ledger.claim("abc", "task-2")
torrent_ledger.claim("unknown", "task-2")
assert torrent_ledger.entries() == {"abc": "task-2"}
def test_forget_drops_only_the_named_hashes():
torrent_ledger.record("a", "t1")
torrent_ledger.record("b", "t2")
torrent_ledger.forget("A")
torrent_ledger.forget()
torrent_ledger.forget("")
assert torrent_ledger.entries() == {"b": "t2"}
def test_an_unusable_ledger_never_raises(monkeypatch, tmp_path):
monkeypatch.setattr(torrent_ledger, "_path", lambda: tmp_path / "missing" / "ledger.db")
torrent_ledger.record("a", "t1")
torrent_ledger.claim("a", "t2")
torrent_ledger.forget("a")
assert torrent_ledger.entries() == {}