diff --git a/pytorrent/services/rtorrent.py b/pytorrent/services/rtorrent.py index 226f866..a4ed462 100644 --- a/pytorrent/services/rtorrent.py +++ b/pytorrent/services/rtorrent.py @@ -371,6 +371,66 @@ def browse_path(profile: dict, path: str | None = None) -> dict: return {"path": base, "parent": parent, "dirs": dirs[:300], "source": "rtorrent"} +POST_CHECK_DOWNLOAD_LABEL = "To download after check" + + +def _label_names(value: str) -> list[str]: + names: list[str] = [] + for part in str(value or "").replace(";", ",").replace("|", ",").split(","): + label = part.strip() + if label and label not in names: + names.append(label) + return names + + +def _label_value(labels: list[str]) -> str: + return ", ".join([label for label in labels if str(label or "").strip()]) + + +def _row_progress_complete(row: dict) -> bool: + size = int(row.get("size") or 0) + completed = int(row.get("completed_bytes") or 0) + return bool(row.get("complete")) or (size > 0 and completed >= size) or float(row.get("progress") or 0) >= 100.0 + + +def apply_post_check_policy(profile: dict, rows: list[dict], previous_rows: dict[str, dict] | None = None) -> list[dict]: + """Start complete torrents after check; pause and label incomplete ones.""" + previous_rows = previous_rows or {} + c = client_for(profile) + changes: list[dict] = [] + for row in rows: + h = str(row.get("hash") or "") + prev = previous_rows.get(h) or {} + was_checking = str(prev.get("status") or "") == "Checking" or int(prev.get("hashing") or 0) > 0 + is_checking = str(row.get("status") or "") == "Checking" or int(row.get("hashing") or 0) > 0 + if not h or not was_checking or is_checking: + continue + complete = _row_progress_complete(row) + try: + if complete: + # Note: Po zakonczonym checku pelny torrent jest automatycznie startowany, zeby od razu seedowal. + c.call("d.start", h) + labels = [label for label in _label_names(str(row.get("label") or "")) if label != POST_CHECK_DOWNLOAD_LABEL] + if _label_value(labels) != str(row.get("label") or ""): + c.call("d.custom1.set", h, _label_value(labels)) + row["label"] = _label_value(labels) + row.update({"state": 1, "active": 1, "paused": False, "status": "Seeding"}) + changes.append({"hash": h, "action": "start", "complete": True}) + else: + # Note: Niepelny torrent po checku trafia do pauzy i dostaje etykiete informujaca, ze wymaga dalszego pobierania. + c.call("d.start", h) + c.call("d.pause", h) + labels = _label_names(str(row.get("label") or "")) + if POST_CHECK_DOWNLOAD_LABEL not in labels: + labels.append(POST_CHECK_DOWNLOAD_LABEL) + c.call("d.custom1.set", h, _label_value(labels)) + row.update({"state": 1, "active": 0, "paused": True, "status": "Paused", "label": _label_value(labels)}) + changes.append({"hash": h, "action": "pause_and_label", "complete": False, "label": POST_CHECK_DOWNLOAD_LABEL}) + except Exception as exc: + changes.append({"hash": h, "action": "post_check_policy_failed", "error": str(exc)}) + return changes + + TORRENT_FIELDS = [ "d.hash=", "d.name=", "d.state=", "d.complete=", "d.size_bytes=", "d.completed_bytes=", "d.ratio=", "d.up.rate=", "d.down.rate=", "d.up.total=", "d.down.total=", "d.peers_connected=", diff --git a/pytorrent/services/torrent_cache.py b/pytorrent/services/torrent_cache.py index c78276f..7e0adae 100644 --- a/pytorrent/services/torrent_cache.py +++ b/pytorrent/services/torrent_cache.py @@ -26,9 +26,11 @@ class TorrentCache: profile_id = int(profile["id"]) try: rows = rtorrent.list_torrents(profile) + with self._lock: + old = dict(self._data.get(profile_id, {})) + post_check_changes = rtorrent.apply_post_check_policy(profile, rows, old) fresh = {t["hash"]: t for t in rows} with self._lock: - old = self._data.get(profile_id, {}) added = [v for h, v in fresh.items() if h not in old] removed = [h for h in old.keys() if h not in fresh] updated = [] @@ -45,7 +47,7 @@ class TorrentCache: self._data[profile_id] = fresh self._errors[profile_id] = "" self._updated_at[profile_id] = time() - return {"ok": True, "profile_id": profile_id, "added": added, "updated": updated, "removed": removed} + return {"ok": True, "profile_id": profile_id, "added": added, "updated": updated, "removed": removed, "post_check_changes": post_check_changes} except Exception as exc: with self._lock: self._errors[profile_id] = str(exc) diff --git a/pytorrent/services/torrent_summary.py b/pytorrent/services/torrent_summary.py index e3476a6..c3f52b8 100644 --- a/pytorrent/services/torrent_summary.py +++ b/pytorrent/services/torrent_summary.py @@ -36,22 +36,28 @@ def _has_error(row: dict) -> bool: return bool(message and any(pattern in message for pattern in _ERROR_PATTERNS)) +def _is_checking(row: dict) -> bool: + return str(row.get("status") or "") == "Checking" or _number(row, "hashing") > 0 + + def _matches(row: dict, summary_type: str) -> bool: status = str(row.get("status") or "") + checking = _is_checking(row) if summary_type == "all": return True if summary_type == "downloading": - return not bool(row.get("complete")) and bool(row.get("state")) and not bool(row.get("paused")) + return not checking and not bool(row.get("complete")) and bool(row.get("state")) and not bool(row.get("paused")) if summary_type == "seeding": - return status != "Checking" and bool(row.get("complete")) and bool(row.get("state")) and not bool(row.get("paused")) + return not checking and bool(row.get("complete")) and bool(row.get("state")) and not bool(row.get("paused")) if summary_type == "paused": - return bool(row.get("paused")) or status == "Paused" + return not checking and (bool(row.get("paused")) or status == "Paused") if summary_type == "checking": - return status == "Checking" or _number(row, "hashing") > 0 + return checking if summary_type == "error": return _has_error(row) if summary_type == "stopped": - return not bool(row.get("state")) + # Note: Stopped count follows the UI filter exactly, so torrents being hash-checked do not inflate an empty Stopped list. + return not checking and not bool(row.get("state")) return False