post-check

This commit is contained in:
Mateusz Gruszczyński
2026-05-24 11:04:42 +02:00
parent 8553615fbf
commit 9caa155324
7 changed files with 47 additions and 16 deletions

View File

@@ -197,8 +197,8 @@ def apply_post_check_policy(profile: dict, rows: list[dict], previous_rows: dict
except Exception: except Exception:
pass pass
c.call("d.custom1.set", h, label_value) c.call("d.custom1.set", h, label_value)
row.update({"state": 0, "active": 0, "paused": False, "status": "Stopped", "label": label_value}) row.update({"state": 0, "active": 0, "paused": False, "post_check": True, "status": "Post-check", "label": label_value})
changes.append({"hash": h, "action": "stop_and_label_after_check", "complete": False, "label": POST_CHECK_DOWNLOAD_LABEL}) changes.append({"hash": h, "action": "mark_post_check_waiting", "complete": False, "label": POST_CHECK_DOWNLOAD_LABEL})
_clear_post_check_watch(profile_id, h) _clear_post_check_watch(profile_id, h)
except Exception as exc: except Exception as exc:
changes.append({"hash": h, "action": "post_check_policy_failed", "error": str(exc)}) changes.append({"hash": h, "action": "post_check_policy_failed", "error": str(exc)})
@@ -267,8 +267,10 @@ def normalize_row(row: list) -> dict:
complete = int(row[3] or 0) complete = int(row[3] or 0)
# Note: d.hashing is authoritative; stale "hash check complete" messages must not keep the UI in Checking forever. # Note: d.hashing is authoritative; stale "hash check complete" messages must not keep the UI in Checking forever.
is_checking = bool(hashing) or _message_indicates_active_check(msg_l) is_checking = bool(hashing) or _message_indicates_active_check(msg_l)
is_paused = bool(state) and not bool(is_active) and not is_checking post_check = POST_CHECK_DOWNLOAD_LABEL in _label_names(str(row[17] or "")) and not is_checking and not bool(is_active)
status = "Checking" if is_checking else "Paused" if is_paused else "Seeding" if complete and state else "Downloading" if state else "Stopped" is_paused = bool(state) and not bool(is_active) and not is_checking and not post_check
# Note: Post-check is an application-level state that separates torrents waiting after a recheck from manually stopped torrents.
status = "Checking" if is_checking else "Post-check" if post_check else "Paused" if is_paused else "Seeding" if complete and state else "Downloading" if state else "Stopped"
to_download_bytes = remaining_bytes if not complete else 0 to_download_bytes = remaining_bytes if not complete else 0
# Note: The To download column is only meaningful for incomplete torrents; complete rows expose an empty display value. # Note: The To download column is only meaningful for incomplete torrents; complete rows expose an empty display value.
return { return {
@@ -305,6 +307,7 @@ def normalize_row(row: list) -> dict:
"ratio_group": str(row[18] or ""), "ratio_group": str(row[18] or ""),
"message": msg, "message": msg,
"status": status, "status": status,
"post_check": post_check,
"hashing": hashing, "hashing": hashing,
} }
@@ -545,12 +548,16 @@ def _download_runtime_state(c: ScgiRtorrentClient, h: str) -> dict:
active = _int_rpc(c, 'd.is_active', h) active = _int_rpc(c, 'd.is_active', h)
opened = _int_rpc(c, 'd.is_open', h) opened = _int_rpc(c, 'd.is_open', h)
# Note: In rTorrent, pause does not change d.state. Paused means state=1, open=1, active=0. # Note: In rTorrent, pause does not change d.state. Paused means state=1, open=1, active=0.
label = _str_rpc(c, 'd.custom1', h)
post_check = POST_CHECK_DOWNLOAD_LABEL in _label_names(label) and not bool(active)
return { return {
'state': state, 'state': state,
'open': opened, 'open': opened,
'active': active, 'active': active,
'paused': bool(state and opened and not active), 'paused': bool(state and opened and not active and not post_check),
'stopped': not bool(state), 'stopped': not bool(state),
'post_check': post_check,
'label': label,
'message': _str_rpc(c, 'd.message', h), 'message': _str_rpc(c, 'd.message', h),
} }
@@ -590,10 +597,14 @@ def stop_hash(c: ScgiRtorrentClient, torrent_hash: str) -> dict:
return {'hash': h, 'ok': False, 'error': 'missing hash'} return {'hash': h, 'ok': False, 'error': 'missing hash'}
before = _download_runtime_state(c, h) before = _download_runtime_state(c, h)
result = {'hash': h, 'before': before, 'commands': []} result = {'hash': h, 'before': before, 'commands': []}
if before.get('stopped'): if before.get('stopped') and not before.get('post_check'):
result.update({'ok': True, 'skipped': 'already_stopped', 'after': before}) result.update({'ok': True, 'skipped': 'already_stopped', 'after': before})
return result return result
try: try:
# Note: User Stop converts the app-level Post-check state into a regular stopped torrent.
if before.get('post_check'):
clear_post_check_download_label(c, h, before.get('label'))
result['commands'].append('clear_post_check_label')
# Note: Smart Queue now enforces the queue with d.stop only; user-paused torrents stay untouched. # Note: Smart Queue now enforces the queue with d.stop only; user-paused torrents stay untouched.
c.call('d.stop', h) c.call('d.stop', h)
result['commands'].append('d.stop') result['commands'].append('d.stop')
@@ -643,10 +654,13 @@ def start_or_resume_hash(c: ScgiRtorrentClient, torrent_hash: str, prefer_start:
result: dict = {'hash': h, 'before': before, 'commands': []} result: dict = {'hash': h, 'before': before, 'commands': []}
if before.get('active'): if before.get('active'):
if before.get('post_check'):
clear_post_check_download_label(c, h, before.get('label'))
before = _download_runtime_state(c, h)
result.update({'ok': True, 'skipped': 'already_active', 'after': before}) result.update({'ok': True, 'skipped': 'already_active', 'after': before})
return result return result
if before.get('paused') and not prefer_start: if before.get('paused') and not prefer_start and not before.get('post_check'):
# Note: Manual Start keeps the clean pause-to-resume path. Do not classify every # Note: Manual Start keeps the clean pause-to-resume path. Do not classify every
# state=1/active=0 item as paused; after auto-check this can be only a transient # state=1/active=0 item as paused; after auto-check this can be only a transient
# open/inactive rTorrent state and needs d.open + d.start. # open/inactive rTorrent state and needs d.open + d.start.
@@ -654,6 +668,13 @@ def start_or_resume_hash(c: ScgiRtorrentClient, torrent_hash: str, prefer_start:
resumed['mode'] = 'resume_paused' resumed['mode'] = 'resume_paused'
return resumed return resumed
if before.get('post_check'):
try:
# Note: Post-check start first forces a clean stopped state, matching the manual Stop -> Start recovery path.
c.call('d.stop', h)
result['commands'].append('d.stop')
except Exception as exc:
result.setdefault('ignored_errors', []).append(f'd.stop: {exc}')
try: try:
c.call('d.open', h) c.call('d.open', h)
result['commands'].append('d.open') result['commands'].append('d.open')
@@ -670,7 +691,13 @@ def start_or_resume_hash(c: ScgiRtorrentClient, torrent_hash: str, prefer_start:
except Exception as exc2: except Exception as exc2:
result.setdefault('ignored_errors', []).append(f'd.try_start: {exc2}') result.setdefault('ignored_errors', []).append(f'd.try_start: {exc2}')
result['ok'] = False result['ok'] = False
result['after'] = _download_runtime_state(c, h) after = _download_runtime_state(c, h)
if before.get('post_check') and after.get('active'):
# Note: The marker stays in place when start fails so the row remains visible in the Post-check filter.
clear_post_check_download_label(c, h, before.get('label'))
result['commands'].append('clear_post_check_label')
after = _download_runtime_state(c, h)
result['after'] = after
result['ok'] = result.get('ok', True) result['ok'] = result.get('ok', True)
return result return result

View File

@@ -19,7 +19,7 @@ _ERROR_PATTERNS = (
"unreachable", "unreachable",
"denied", "denied",
) )
_SUMMARY_TYPES = ("all", "downloading", "seeding", "paused", "checking", "error", "stopped") _SUMMARY_TYPES = ("all", "downloading", "seeding", "paused", "checking", "error", "post_check", "stopped")
_summary_cache: dict[int, dict] = {} _summary_cache: dict[int, dict] = {}
_summary_lock = RLock() _summary_lock = RLock()
@@ -55,9 +55,12 @@ def _matches(row: dict, summary_type: str) -> bool:
return checking return checking
if summary_type == "error": if summary_type == "error":
return _has_error(row) return _has_error(row)
if summary_type == "post_check":
# Note: Post-check is counted separately from Stopped so automation can target it safely.
return str(row.get("status") or "") == "Post-check" or bool(row.get("post_check"))
if summary_type == "stopped": if summary_type == "stopped":
# Note: Stopped count follows the UI filter exactly, so torrents being hash-checked do not inflate an empty Stopped list. # Note: Stopped count follows the UI filter exactly and excludes app-level post-check waiting rows.
return not checking and not bool(row.get("state")) return not checking and not bool(row.get("state")) and str(row.get("status") or "") != "Post-check" and not bool(row.get("post_check"))
return False return False

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long