torrent-audit: only filter out complete torrents
This commit is contained in:
@@ -57,6 +57,19 @@ def get_qbit_torrents(qbit_client, category: str) -> dict[str, dict]:
|
||||
return {t["hash"].upper(): t for t in torrents}
|
||||
|
||||
|
||||
def is_complete(torrent: dict) -> bool:
|
||||
"""True iff the torrent's payload is fully on disk.
|
||||
|
||||
A torrent that was once imported can later end up at progress < 1 if the
|
||||
files were deleted or qBittorrent was reset and the torrent was re-added.
|
||||
Those entries must NOT be reported as abandoned-safe: their reported size
|
||||
is the metadata size, not what is actually on disk, so the reclaim figure
|
||||
would be a fiction and a 'safe to delete' verdict could kill a re-grab in
|
||||
progress.
|
||||
"""
|
||||
return float(torrent.get("progress", 0)) >= 1.0
|
||||
|
||||
|
||||
def gib(size_bytes: int) -> str:
|
||||
return f"{size_bytes / 1073741824:.1f}"
|
||||
|
||||
@@ -133,6 +146,12 @@ def find_movie_abandoned(radarr, qbit_movies):
|
||||
torrent = qbit_movies.get(ahash)
|
||||
if torrent is None:
|
||||
continue
|
||||
# Skip torrents whose payload is not fully on disk: their reported size
|
||||
# is metadata, not actual on-disk bytes, so flagging them as
|
||||
# abandoned-safe would lie about the reclaim and could disrupt a
|
||||
# re-download in progress.
|
||||
if not is_complete(torrent):
|
||||
continue
|
||||
|
||||
mid = hash_to_movie.get(ahash)
|
||||
movie = radarr_movies.get(mid) if mid else None
|
||||
@@ -211,6 +230,8 @@ def find_tv_abandoned(sonarr, qbit_tvshows):
|
||||
torrent = qbit_tvshows.get(ahash)
|
||||
if torrent is None:
|
||||
continue
|
||||
if not is_complete(torrent):
|
||||
continue
|
||||
|
||||
status = "SAFE"
|
||||
notes = []
|
||||
|
||||
@@ -52,6 +52,7 @@ let
|
||||
SINGLE_CROSS = "A" * 38 + "0C" # movieId=7 single import AND older import for movieId=8
|
||||
SINGLE8_NEW = "A" * 38 + "0D" # movieId=8, newer import → keeper (not in qBit)
|
||||
QUEUED_MOV = "A" * 38 + "0E" # in Radarr queue, not in history
|
||||
INPROGRESS_MOV = "A" * 38 + "0F" # movieId=10, older import, currently re-downloading
|
||||
|
||||
# TV
|
||||
UNMANAGED_TV = "B" * 38 + "01"
|
||||
@@ -62,13 +63,17 @@ let
|
||||
REPACK = "B" * 38 + "06" # episodeId=300, newer import → active
|
||||
REMOVED_TV = "B" * 38 + "07" # episodeId=400, older import (series removed)
|
||||
REMOVED_TV_NEW = "B" * 38 + "08" # episodeId=400, newer import (not in qBit)
|
||||
INPROGRESS_TV = "B" * 38 + "09" # episodeId=500, older import, currently re-downloading
|
||||
INPROGRESS_TV_NEW = "B" * 38 + "0A" # episodeId=500, newer import (not in qBit)
|
||||
INPROGRESS_MOV_NEW = "A" * 38 + "10" # movieId=10, newer import (not in qBit)
|
||||
|
||||
def make_torrent(h, name, size, added_on, state="uploading"):
|
||||
def make_torrent(h, name, size, added_on, state="uploading", progress=1.0):
|
||||
return {
|
||||
"hash": h.lower(),
|
||||
"name": name,
|
||||
"size": size,
|
||||
"state": state,
|
||||
"progress": progress,
|
||||
"added_on": added_on,
|
||||
"content_path": f"/downloads/{name}",
|
||||
}
|
||||
@@ -84,6 +89,9 @@ let
|
||||
make_torrent(LARGER_OLD, "Larger.Movie.2024", 10_737_418_240, 1704067206),
|
||||
make_torrent(SINGLE_CROSS, "SingleCross.Movie.2024", 4_000_000_000, 1704067207),
|
||||
make_torrent(QUEUED_MOV, "Queued.Movie.2024", 2_000_000_000, 1704067208),
|
||||
# In-progress re-download: hash matches an old import, but data is
|
||||
# not yet on disk. Must NOT be flagged as abandoned (regression).
|
||||
make_torrent(INPROGRESS_MOV, "InProgress.Movie.2024", 8_000_000_000, 1704067209, state="downloading", progress=0.05),
|
||||
],
|
||||
"tvshows": [
|
||||
make_torrent(UNMANAGED_TV, "Unmanaged.Show.S01E01", 1_000_000_000, 1704067200),
|
||||
@@ -92,6 +100,7 @@ let
|
||||
make_torrent(NEW_TV, "New.Show.S01E01", 1_200_000_000, 1704067203),
|
||||
make_torrent(SEASON_PACK, "Season.Pack.S02", 5_000_000_000, 1704067204),
|
||||
make_torrent(REMOVED_TV, "Removed.Show.S01E01", 900_000_000, 1704067205),
|
||||
make_torrent(INPROGRESS_TV, "InProgress.Show.S01E01", 1_500_000_000, 1704067209, state="downloading", progress=0.05),
|
||||
],
|
||||
}
|
||||
|
||||
@@ -115,6 +124,9 @@ let
|
||||
{"movieId": 7, "downloadId": SINGLE_CROSS, "eventType": "downloadFolderImported", "date": "2024-03-01T00:00:00Z"},
|
||||
{"movieId": 8, "downloadId": SINGLE_CROSS, "eventType": "downloadFolderImported", "date": "2024-01-01T00:00:00Z"},
|
||||
{"movieId": 8, "downloadId": SINGLE8_NEW, "eventType": "downloadFolderImported", "date": "2024-06-01T00:00:00Z"},
|
||||
# In-progress re-download regression case for movies
|
||||
{"movieId": 10, "downloadId": INPROGRESS_MOV, "eventType": "downloadFolderImported", "date": "2024-01-01T00:00:00Z"},
|
||||
{"movieId": 10, "downloadId": INPROGRESS_MOV_NEW,"eventType": "downloadFolderImported", "date": "2024-06-01T00:00:00Z"},
|
||||
]
|
||||
|
||||
RADARR_MOVIES = [
|
||||
@@ -126,6 +138,7 @@ let
|
||||
{"id": 6, "hasFile": True, "movieFile": {"size": 5_368_709_120, "quality": {"quality": {"name": "Bluray-720p"}}}},
|
||||
{"id": 7, "hasFile": True, "movieFile": {"size": 4_000_000_000, "quality": {"quality": {"name": "Bluray-1080p"}}}},
|
||||
{"id": 8, "hasFile": True, "movieFile": {"size": 5_000_000_000, "quality": {"quality": {"name": "Remux-1080p"}}}},
|
||||
{"id": 10, "hasFile": True, "movieFile": {"size": 8_000_000_000, "quality": {"quality": {"name": "Remux-2160p"}}}},
|
||||
]
|
||||
|
||||
# ── Sonarr mock data ──────────────────────────────────────────────────
|
||||
@@ -148,6 +161,9 @@ let
|
||||
# Removed series scenario
|
||||
{"episodeId": 400, "seriesId": 99, "downloadId": REMOVED_TV, "eventType": "downloadFolderImported", "date": "2024-01-01T00:00:00Z"},
|
||||
{"episodeId": 400, "seriesId": 99, "downloadId": REMOVED_TV_NEW,"eventType": "downloadFolderImported", "date": "2024-06-01T00:00:00Z"},
|
||||
# In-progress re-download regression case for TV
|
||||
{"episodeId": 500, "seriesId": 1, "downloadId": INPROGRESS_TV, "eventType": "downloadFolderImported", "date": "2024-01-01T00:00:00Z"},
|
||||
{"episodeId": 500, "seriesId": 1, "downloadId": INPROGRESS_TV_NEW,"eventType": "downloadFolderImported", "date": "2024-06-01T00:00:00Z"},
|
||||
]
|
||||
SONARR_HISTORY_ALL = SONARR_HISTORY_PAGE1 + SONARR_HISTORY_PAGE2
|
||||
|
||||
@@ -319,14 +335,14 @@ pkgs.testers.runNixOSTest {
|
||||
with subtest("Detects unmanaged movie torrent"):
|
||||
assert "Unmanaged.Movie.2024" in unmanaged_section, \
|
||||
"Should detect unmanaged movie"
|
||||
assert "1 unmanaged / 9 total" in unmanaged_section, \
|
||||
"Should show 1 unmanaged movie out of 9"
|
||||
assert "1 unmanaged / 10 total" in unmanaged_section, \
|
||||
"Should show 1 unmanaged movie out of 10"
|
||||
|
||||
with subtest("Detects unmanaged TV torrent"):
|
||||
assert "Unmanaged.Show.S01E01" in unmanaged_section, \
|
||||
"Should detect unmanaged TV show"
|
||||
assert "1 unmanaged / 6 total" in unmanaged_section, \
|
||||
"Should show 1 unmanaged TV show out of 6"
|
||||
assert "1 unmanaged / 7 total" in unmanaged_section, \
|
||||
"Should show 1 unmanaged TV show out of 7"
|
||||
|
||||
with subtest("Empty category shows zero counts"):
|
||||
assert "0 unmanaged / 0 total" in unmanaged_section, \
|
||||
@@ -380,6 +396,16 @@ pkgs.testers.runNixOSTest {
|
||||
assert "SingleCross.Movie.2024" not in abandoned_section, \
|
||||
"Hash that is sole import for movieId=7 must be in keeper set, not abandoned"
|
||||
|
||||
with subtest("In-progress re-download not abandoned (incomplete payload regression)"):
|
||||
# A torrent whose hash matches an old downloadFolderImported entry but
|
||||
# whose data is not currently on disk (progress < 1.0) must not be
|
||||
# reported as abandoned: its size is metadata, not reclaimable bytes,
|
||||
# and a SAFE verdict could disrupt a re-download in progress.
|
||||
assert "InProgress.Movie.2024" not in abandoned_section, \
|
||||
"In-progress movie re-download must not appear as abandoned"
|
||||
assert "InProgress.Show.S01E01" not in abandoned_section, \
|
||||
"In-progress TV re-download must not appear as abandoned"
|
||||
|
||||
with subtest("Removed movie triggers REVIEW status"):
|
||||
assert "Removed.Movie.2024" in abandoned_section, \
|
||||
"Should detect abandoned torrent for removed movie"
|
||||
|
||||
Reference in New Issue
Block a user