fix(torrent): use before/after diff to recover qBit info-hash
Live testing surfaced: every download attempt failed with 'Torrent client refused the URL', but qBit was actually accepting the add request fine. The bug was in our hash-lookup strategy. qBittorrent's /api/v2/torrents/add returns 200 'Ok.' regardless of whether the URL was actually valid / accepted / registered. The previous code then queried /torrents/info?category=soulsync to find the just-added torrent — but qBit hadn't categorised the new torrent yet on the first poll, AND a fresh install has no 'soulsync' category configured, so the lookup returned empty and the adapter reported failure for every working download. New strategy: - Snapshot every torrent hash qBit currently tracks BEFORE posting to /add. - POST /add, accept its (uninformative) 200 OK. - Poll the all-torrents list for up to 5 seconds, looking for a hash that wasn't present in the before-snapshot. First new hash wins. The diff strategy works the same for /add with urls= (HTTP URL / magnet) and /add with files= (raw .torrent upload), so both paths now share the same _all_hashes + _poll_for_new_hash helpers. Adds a warning log when qBit returns an unexpected body and an error log when no new hash appears (so future investigation has breadcrumbs).
This commit is contained in:
parent
478fd25dd6
commit
e83b661471
1 changed files with 49 additions and 13 deletions
|
|
@ -164,28 +164,60 @@ class QBittorrentAdapter:
|
|||
category: str,
|
||||
save_path: Optional[str],
|
||||
) -> Optional[str]:
|
||||
data = {'urls': url_or_magnet, 'category': category or self._category}
|
||||
cat = category or self._category
|
||||
# Snapshot the current set of torrent hashes BEFORE adding —
|
||||
# qBittorrent's /add endpoint returns 200 "Ok." regardless of
|
||||
# whether the URL was actually accepted or registered, and
|
||||
# category-filtered lookups race the add (qBit hasn't
|
||||
# categorised the new torrent yet on the first poll). Diffing
|
||||
# before / after is the only reliable way to recover the hash.
|
||||
before = self._all_hashes()
|
||||
if before is None:
|
||||
return None
|
||||
data = {'urls': url_or_magnet, 'category': cat}
|
||||
if save_path or self._save_path:
|
||||
data['savepath'] = save_path or self._save_path
|
||||
resp = self._call('POST', '/api/v2/torrents/add', data=data)
|
||||
if not resp or not resp.ok:
|
||||
logger.warning("qBittorrent /torrents/add returned HTTP %s body=%r",
|
||||
resp.status_code if resp else 'no-response',
|
||||
(resp.text[:200] if resp else ''))
|
||||
return None
|
||||
# qBittorrent's /add endpoint returns 200 "Ok." on success but
|
||||
# NOT the resulting info-hash. We have to look it up via
|
||||
# /torrents/info filtered by category — the just-added torrent
|
||||
# will be the newest entry in that category.
|
||||
return self._lookup_latest_hash(category or self._category)
|
||||
if resp.text and resp.text.strip() and resp.text.strip() != 'Ok.':
|
||||
logger.warning("qBittorrent /torrents/add unexpected body: %r", resp.text[:200])
|
||||
return None
|
||||
new_hash = self._poll_for_new_hash(before)
|
||||
if not new_hash:
|
||||
logger.error("qBittorrent accepted the request but no new torrent appeared — "
|
||||
"URL may have been rejected (bad magnet, unreachable HTTPS, "
|
||||
"duplicate hash, etc.)")
|
||||
return new_hash
|
||||
|
||||
def _lookup_latest_hash(self, category: str) -> Optional[str]:
|
||||
resp = self._call('GET', '/api/v2/torrents/info', params={'category': category, 'sort': 'added_on', 'reverse': 'true', 'limit': 1})
|
||||
def _all_hashes(self) -> Optional[set]:
|
||||
"""Return the set of every torrent hash qBit currently tracks,
|
||||
or None on lookup failure."""
|
||||
resp = self._call('GET', '/api/v2/torrents/info')
|
||||
if not resp or not resp.ok:
|
||||
return None
|
||||
try:
|
||||
items = resp.json()
|
||||
if items:
|
||||
return items[0].get('hash')
|
||||
return {item.get('hash') for item in resp.json() if item.get('hash')}
|
||||
except Exception as e:
|
||||
logger.error("qBittorrent /torrents/info parse failed: %s", e)
|
||||
return None
|
||||
|
||||
def _poll_for_new_hash(self, before: set) -> Optional[str]:
|
||||
"""Poll up to ~5s for a new torrent to appear (qBit takes a
|
||||
moment to fetch the .torrent file from the URL and register
|
||||
it). Returns the new hash, or None if nothing showed up."""
|
||||
import time as _time
|
||||
for _ in range(10):
|
||||
_time.sleep(0.5)
|
||||
current = self._all_hashes()
|
||||
if current is None:
|
||||
continue
|
||||
new = current - before
|
||||
if new:
|
||||
return next(iter(new))
|
||||
return None
|
||||
|
||||
async def add_torrent_file(
|
||||
|
|
@ -205,14 +237,18 @@ class QBittorrentAdapter:
|
|||
category: str,
|
||||
save_path: Optional[str],
|
||||
) -> Optional[str]:
|
||||
data = {'category': category or self._category}
|
||||
cat = category or self._category
|
||||
before = self._all_hashes()
|
||||
if before is None:
|
||||
return None
|
||||
data = {'category': cat}
|
||||
if save_path or self._save_path:
|
||||
data['savepath'] = save_path or self._save_path
|
||||
files = {'torrents': ('soulsync.torrent', file_bytes, 'application/x-bittorrent')}
|
||||
resp = self._call('POST', '/api/v2/torrents/add', data=data, files=files)
|
||||
if not resp or not resp.ok:
|
||||
return None
|
||||
return self._lookup_latest_hash(category or self._category)
|
||||
return self._poll_for_new_hash(before)
|
||||
|
||||
async def get_status(self, torrent_id: str) -> Optional[TorrentStatus]:
|
||||
loop = asyncio.get_event_loop()
|
||||
|
|
|
|||
Loading…
Reference in a new issue