From 6446846ee8199179d85db4f9a3e32a68281e13c2 Mon Sep 17 00:00:00 2001 From: Cabbagec Date: Mon, 3 Aug 2026 07:01:10 +0000 Subject: [PATCH] Accept qBittorrent hidden padding files --- src/archive_clients/resources.py | 13 ++++++++-- tests/test_resources.py | 41 ++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/src/archive_clients/resources.py b/src/archive_clients/resources.py index 1dcd54a..9e6131f 100644 --- a/src/archive_clients/resources.py +++ b/src/archive_clients/resources.py @@ -91,12 +91,21 @@ def normalize_resource( observed_at: datetime | None = None, ) -> NormalizedResource: metainfo = decode_metainfo(metainfo_bytes) - if len(raw_files) != len(metainfo.files): + # qBittorrent/libtorrent may omit torrent padding files from + # /torrents/files while keeping them in the exported metainfo. + visible_metainfo_files = tuple( + item for item in metainfo.files if not item.padding + ) + if len(raw_files) == len(metainfo.files): + matched_metainfo_files = metainfo.files + elif len(raw_files) == len(visible_metainfo_files): + matched_metainfo_files = visible_metainfo_files + else: raise ResourceError("qBittorrent and metainfo file counts differ") files = [] canonical = True for expected_index, (raw, meta_file) in enumerate( - zip(raw_files, metainfo.files, strict=True) + zip(raw_files, matched_metainfo_files, strict=True) ): index = _integer(raw.get("index"), "file index") if index != expected_index: diff --git a/tests/test_resources.py b/tests/test_resources.py index 03ec030..0618aec 100644 --- a/tests/test_resources.py +++ b/tests/test_resources.py @@ -118,6 +118,47 @@ class ResourceTests(unittest.TestCase): self.assertEqual(root.available_file_count, 1) self.assertEqual(root.available_logical_bytes, 3) + def test_qbittorrent_hidden_padding_files_are_normalized(self): + info = { + b"files": [ + {b"length": 3, b"path": [b"first.bin"]}, + { + b"attr": b"p", b"length": 5, + b"path": [b"_____padding_file_5"], + }, + {b"length": 7, b"path": [b"last.bin"]}, + ], + b"name": b"with-padding", + b"piece length": 16384, + b"pieces": b"x" * 20, + } + metainfo = encode({b"info": info}) + torrent_hash = hashlib.sha1(encode(info)).hexdigest() + normalized = normalize_resource( + { + "hash": torrent_hash, + "name": "with-padding", + "state": "stalledUP", + }, + [ + { + "index": 0, "name": "with-padding/first.bin", + "size": 3, "completed": 3, "priority": 1, + }, + { + "index": 1, "name": "with-padding/last.bin", + "size": 7, "completed": 7, "priority": 1, + }, + ], + metainfo, + ) + self.assertEqual( + [item.canonical_path for item in normalized.files], + ["with-padding/first.bin", "with-padding/last.bin"], + ) + self.assertEqual(normalized.summary.total_file_count, 2) + self.assertEqual(normalized.summary.selected_complete_bytes, 10) + def test_noncanonical_and_unsafe_paths_are_distinct(self): info = { b"length": 3,