fix in peers
This commit is contained in:
+694
-300
File diff suppressed because it is too large
Load Diff
@@ -483,13 +483,15 @@ def torrent_peers(torrent_hash: str):
|
|||||||
if not profile:
|
if not profile:
|
||||||
return jsonify({"ok": False, "error": "No profile"}), 400
|
return jsonify({"ok": False, "error": "No profile"}), 400
|
||||||
peers = rtorrent.torrent_peers(profile, torrent_hash)
|
peers = rtorrent.torrent_peers(profile, torrent_hash)
|
||||||
|
activity = rtorrent.torrent_peer_activity(profile, torrent_hash)
|
||||||
for peer in peers:
|
for peer in peers:
|
||||||
peer.update(lookup_ip(peer.get("ip", "")))
|
peer.update(lookup_ip(peer.get("ip", "")))
|
||||||
prefs = preferences.get_preferences(profile_id=profile.get("id"))
|
prefs = preferences.get_preferences(profile_id=profile.get("id"))
|
||||||
if int(prefs.get("reverse_dns_enabled") or 0):
|
if int(prefs.get("reverse_dns_enabled") or 0):
|
||||||
# Note: PTR hostnames are attached only when the user enables the lightweight cached resolver.
|
# Note: PTR hostnames are attached only when the user enables the lightweight cached resolver.
|
||||||
attach_reverse_dns(peers)
|
attach_reverse_dns(peers)
|
||||||
return ok({"peers": peers})
|
# Note: peer_activity lets the UI silently retry when rTorrent reports traffic before peer rows are visible.
|
||||||
|
return ok({"peers": peers, "peer_activity": activity})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -400,17 +400,42 @@ def list_torrents(profile: dict) -> list[dict]:
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
def torrent_peers(profile: dict, torrent_hash: str) -> list[dict]:
|
PEER_DETAIL_FIELDS = [
|
||||||
fields = [
|
|
||||||
"p.address=", "p.client_version=", "p.completed_percent=", "p.down_rate=",
|
"p.address=", "p.client_version=", "p.completed_percent=", "p.down_rate=",
|
||||||
"p.up_rate=", "p.port=", "p.is_encrypted=", "p.is_incoming=",
|
"p.up_rate=", "p.port=", "p.is_encrypted=", "p.is_incoming=",
|
||||||
"p.is_snubbed=", "p.is_banned=",
|
"p.is_snubbed=", "p.is_banned=",
|
||||||
]
|
]
|
||||||
|
PEER_DETAIL_FALLBACK_FIELDS = [
|
||||||
|
"p.address=", "p.client_version=", "p.completed_percent=", "p.down_rate=",
|
||||||
|
"p.up_rate=", "p.port=", "p.is_encrypted=",
|
||||||
|
]
|
||||||
|
PEER_ACTIVITY_FIELDS = ["d.peers_connected=", "d.up.rate=", "d.down.rate=", "d.is_active="]
|
||||||
|
|
||||||
|
|
||||||
|
def _peer_rows(c: ScgiRtorrentClient, torrent_hash: str) -> list:
|
||||||
try:
|
try:
|
||||||
rows = client_for(profile).p.multicall(torrent_hash, "", *fields)
|
return c.p.multicall(torrent_hash, "", *PEER_DETAIL_FIELDS)
|
||||||
except Exception:
|
except Exception:
|
||||||
fields = ["p.address=", "p.client_version=", "p.completed_percent=", "p.down_rate=", "p.up_rate=", "p.port=", "p.is_encrypted="]
|
return c.p.multicall(torrent_hash, "", *PEER_DETAIL_FALLBACK_FIELDS)
|
||||||
rows = client_for(profile).p.multicall(torrent_hash, "", *fields)
|
|
||||||
|
|
||||||
|
def _peer_activity(c: ScgiRtorrentClient, torrent_hash: str) -> dict:
|
||||||
|
try:
|
||||||
|
values = c.d.multicall2("", "main", "d.hash=", *PEER_ACTIVITY_FIELDS)
|
||||||
|
for row in values:
|
||||||
|
if str(row[0] or "").lower() == str(torrent_hash or "").lower():
|
||||||
|
return {
|
||||||
|
"peers_connected": int(row[1] or 0),
|
||||||
|
"up_rate": int(row[2] or 0),
|
||||||
|
"down_rate": int(row[3] or 0),
|
||||||
|
"active": bool(row[4]),
|
||||||
|
}
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return {"peers_connected": 0, "up_rate": 0, "down_rate": 0, "active": False}
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_peer_rows(rows: list) -> list[dict]:
|
||||||
peers = []
|
peers = []
|
||||||
for idx, r in enumerate(rows):
|
for idx, r in enumerate(rows):
|
||||||
peers.append({
|
peers.append({
|
||||||
@@ -431,6 +456,30 @@ def torrent_peers(profile: dict, torrent_hash: str) -> list[dict]:
|
|||||||
return peers
|
return peers
|
||||||
|
|
||||||
|
|
||||||
|
def torrent_peer_activity(profile: dict, torrent_hash: str) -> dict:
|
||||||
|
"""Return lightweight live counters used to decide whether an empty peer list may be stale."""
|
||||||
|
# Note: This is additive and does not change torrent list polling or cached torrent rows.
|
||||||
|
return _peer_activity(client_for(profile), torrent_hash)
|
||||||
|
|
||||||
|
|
||||||
|
def torrent_peers(profile: dict, torrent_hash: str, retry_when_active: bool = True) -> list[dict]:
|
||||||
|
c = client_for(profile)
|
||||||
|
rows = _peer_rows(c, torrent_hash)
|
||||||
|
if rows or not retry_when_active:
|
||||||
|
return _normalize_peer_rows(rows)
|
||||||
|
activity = _peer_activity(c, torrent_hash)
|
||||||
|
should_retry = bool(activity.get("peers_connected") or activity.get("up_rate") or activity.get("down_rate"))
|
||||||
|
if not should_retry:
|
||||||
|
return []
|
||||||
|
# Note: rTorrent can expose transfer counters before p.multicall catches up; short retries avoid a misleading empty peer table.
|
||||||
|
for _attempt in range(3):
|
||||||
|
time.sleep(0.2)
|
||||||
|
rows = _peer_rows(c, torrent_hash)
|
||||||
|
if rows:
|
||||||
|
return _normalize_peer_rows(rows)
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _call_first(c: ScgiRtorrentClient, candidates: list[tuple[str, tuple]]) -> dict:
|
def _call_first(c: ScgiRtorrentClient, candidates: list[tuple[str, tuple]]) -> dict:
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export const peerRefreshSource = " function setupPeersRefresh(tab=activeTab()){ clearInterval(peersRefreshTimer); peersRefreshTimer=null; if($('peersRefreshSelect')) $('peersRefreshSelect').value=String(peersRefreshSeconds||0); if(tab==='peers' && peersRefreshSeconds>0){ peersRefreshTimer=setInterval(()=>{ if(activeTab()==='peers' && selectedHash) loadDetails('peers'); }, peersRefreshSeconds*1000); } }\n function refreshPeersOnceForReverseDns(){\n // Note: Enabling reverse DNS immediately refreshes peers; pending hostnames then use their own follow-up loop.\n if(activeTab()==='peers' && selectedHash) loadDetails('peers');\n const modal=$('mobileDetailsModal');\n if(modal?.classList.contains('show') && selectedHash) openMobileDetails(selectedHash);\n }\n function syncMobileMode(){ const auto=window.matchMedia&&window.matchMedia(\"(max-width: 900px)\").matches; document.body.classList.toggle(\"mobile-mode\", auto || document.body.classList.contains(\"mobile-mode-manual\")); scheduleRender(true); }\n\n\n let automationRulesCache=[];\n let automationConditions=[];\n let automationEffects=[];\n";
|
export const peerRefreshSource = " function setupPeersRefresh(tab=activeTab()){ clearInterval(peersRefreshTimer); peersRefreshTimer=null; if($('peersRefreshSelect')) $('peersRefreshSelect').value=String(peersRefreshSeconds||0); if(tab==='peers' && peersRefreshSeconds>0){ peersRefreshTimer=setInterval(()=>{ if(activeTab()==='peers' && selectedHash) loadDetails('peers',{silent:true}); }, peersRefreshSeconds*1000); } }\n function clearPeerEmptyRetry(){\n clearTimeout(peerEmptyRetryTimer);\n peerEmptyRetryTimer=null;\n peerEmptyRetryHash=null;\n peerEmptyRetryAttempts=0;\n }\n function peerActivityLooksActive(activity){\n return !!(activity && (Number(activity.peers_connected||0)>0 || Number(activity.up_rate||0)>0 || Number(activity.down_rate||0)>0));\n }\n function schedulePeerEmptyRetry(peers, activity){\n if((peers||[]).length || !peerActivityLooksActive(activity)){ clearPeerEmptyRetry(); return; }\n if(activeTab()!=='peers' || !selectedHash) return;\n const hash=selectedHash;\n if(peerEmptyRetryHash!==hash){ peerEmptyRetryHash=hash; peerEmptyRetryAttempts=0; }\n if(peerEmptyRetryTimer || peerEmptyRetryAttempts>=PEER_EMPTY_RETRY_MAX_ATTEMPTS) return;\n peerEmptyRetryAttempts+=1;\n peerEmptyRetryTimer=setTimeout(async()=>{\n peerEmptyRetryTimer=null;\n if(activeTab()!=='peers' || selectedHash!==hash) return;\n await loadDetails('peers',{silent:true});\n }, PEER_EMPTY_RETRY_SECONDS*1000);\n }\n function refreshPeersOnceForReverseDns(){\n // Note: Enabling reverse DNS immediately refreshes peers; pending hostnames then use their own follow-up loop.\n if(activeTab()==='peers' && selectedHash) loadDetails('peers');\n const modal=$('mobileDetailsModal');\n if(modal?.classList.contains('show') && selectedHash) openMobileDetails(selectedHash);\n }\n function syncMobileMode(){ const auto=window.matchMedia&&window.matchMedia(\"(max-width: 900px)\").matches; document.body.classList.toggle(\"mobile-mode\", auto || document.body.classList.contains(\"mobile-mode-manual\")); scheduleRender(true); }\n\n\n let automationRulesCache=[];\n let automationConditions=[];\n let automationEffects=[];\n";
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
|||||||
export const torrentDetailsLoaderSource = " async function loadDetails(tab, options={}){\n const t=torrents.get(selectedHash);\n const silent = !!options.silent;\n if(tab !== 'files') clearFilesAutoRefresh();\n if(tab !== 'peers') clearReverseDnsPeerRefresh();\n if($('peersRefreshBox')) $('peersRefreshBox').classList.toggle('d-none', tab!=='peers');\n setupPeersRefresh(tab);\n if(!t) return;\n if(tab==='general') return renderGeneral();\n if(tab==='log'){\n $('detailPane').innerHTML=`<pre class=\"torrent-log-message\">${esc(t.message||'No logs')}</pre>`;\n return;\n }\n const pane=$('detailPane');\n if(!silent) pane.innerHTML=`<div class=\"loading-line\"><span class=\"spinner-border spinner-border-sm\"></span> Loading ${esc(tab)}...</div>`;\n try{\n const detailUrl = tab==='chunks' ? `/api/torrents/${encodeURIComponent(selectedHash)}/chunks?max_cells=${chunkMaxCellsForDensity()}` : `/api/torrents/${encodeURIComponent(selectedHash)}/${tab}`;\n const res=await fetch(detailUrl,{headers:{'Accept':'application/json'}});\n const text=await res.text();\n let json;\n try{\n json=JSON.parse(text);\n }catch(parseErr){\n throw new Error(`Invalid API response for ${tab}. HTTP ${res.status}`);\n }\n if(!res.ok || !json.ok) throw new Error(json.error||`HTTP ${res.status}`);\n if(tab!==activeTab()) return;\n if(tab==='files') renderFiles(json.files||[]);\n if(tab==='chunks') renderChunks(json.chunks||{});\n if(tab==='peers') renderPeers(json.peers||[]);\n if(tab==='trackers') renderTrackers(json.trackers||[]);\n }catch(e){\n if(!silent) pane.innerHTML=`<div class=\"text-danger\">${esc(e.message)}</div>`;\n }\n }\n";
|
export const torrentDetailsLoaderSource = " async function loadDetails(tab, options={}){\n const t=torrents.get(selectedHash);\n const silent = !!options.silent;\n if(tab !== 'files') clearFilesAutoRefresh();\n if(tab !== 'peers'){ clearReverseDnsPeerRefresh(); clearPeerEmptyRetry(); }\n if($('peersRefreshBox')) $('peersRefreshBox').classList.toggle('d-none', tab!=='peers');\n setupPeersRefresh(tab);\n if(!t) return;\n if(tab==='general') return renderGeneral();\n if(tab==='log'){\n $('detailPane').innerHTML=`<pre class=\"torrent-log-message\">${esc(t.message||'No logs')}</pre>`;\n return;\n }\n const pane=$('detailPane');\n const samePeerHash = tab==='peers' && pane?.dataset?.peersHash===selectedHash;\n const hasPeerContent = samePeerHash && pane?.dataset?.peersLoaded==='1';\n if(!silent && !hasPeerContent) pane.innerHTML=`<div class=\"loading-line\"><span class=\"spinner-border spinner-border-sm\"></span> Loading ${esc(tab)}...</div>`;\n try{\n const detailUrl = tab==='chunks' ? `/api/torrents/${encodeURIComponent(selectedHash)}/chunks?max_cells=${chunkMaxCellsForDensity()}` : `/api/torrents/${encodeURIComponent(selectedHash)}/${tab}`;\n const res=await fetch(detailUrl,{headers:{'Accept':'application/json'}});\n const text=await res.text();\n let json;\n try{\n json=JSON.parse(text);\n }catch(parseErr){\n throw new Error(`Invalid API response for ${tab}. HTTP ${res.status}`);\n }\n if(!res.ok || !json.ok) throw new Error(json.error||`HTTP ${res.status}`);\n if(tab!==activeTab()) return;\n if(tab==='files') renderFiles(json.files||[]);\n if(tab==='chunks') renderChunks(json.chunks||{});\n if(tab==='peers'){ pane.dataset.peersLoaded='1'; pane.dataset.peersHash=selectedHash; renderPeers(json.peers||[], json.peer_activity||null); }\n if(tab==='trackers') renderTrackers(json.trackers||[]);\n }catch(e){\n if(!silent) pane.innerHTML=`<div class=\"text-danger\">${esc(e.message)}</div>`;\n }\n }\n";
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export const torrentPeerDetailsSource = " function peerBadges(p){\n const badges=[];\n if(p.encrypted) badges.push('<span class=\"badge text-bg-success\">enc</span>');\n if(p.incoming) badges.push('<span class=\"badge text-bg-info\">in</span>');\n if(p.snubbed) badges.push('<span class=\"badge text-bg-warning\">snub</span>');\n if(p.banned) badges.push('<span class=\"badge text-bg-danger\">ban</span>');\n return badges.join(' ') || '<span class=\"text-muted\">-</span>';\n }\n function peerHostCell(p){\n const host=String(p.host||'').trim();\n // Note: Hostnames use the available peer-table space instead of a fixed character-style cap.\n if(host) return `<span class=\"peer-host\" title=\"${esc(host)}\">${esc(host)}</span>`;\n if(p.host_pending) return '<span class=\"text-muted\">resolving</span>';\n return '<span class=\"text-muted\">-</span>';\n }\n function hasPendingReverseDns(peers){\n return reverseDnsEnabled && (peers||[]).some(p=>p && p.host_pending);\n }\n function clearReverseDnsPeerRefresh(){\n clearTimeout(reverseDnsRefreshTimer);\n reverseDnsRefreshTimer=null;\n reverseDnsRefreshInFlight=false;\n reverseDnsRefreshAttempts=0;\n reverseDnsRefreshHash=null;\n }\n function scheduleReverseDnsPeerRefresh(peers){\n // Note: PTR results are checked on a short independent loop, not on the manual/auto peers refresh interval.\n if(!hasPendingReverseDns(peers)){ clearReverseDnsPeerRefresh(); return; }\n if(activeTab()!=='peers' || !selectedHash) return;\n const hash=selectedHash;\n if(reverseDnsRefreshHash!==hash){ reverseDnsRefreshHash=hash; reverseDnsRefreshAttempts=0; }\n if(reverseDnsRefreshTimer || reverseDnsRefreshAttempts>=REVERSE_DNS_REFRESH_MAX_ATTEMPTS) return;\n reverseDnsRefreshAttempts+=1;\n reverseDnsRefreshTimer=setTimeout(async()=>{\n reverseDnsRefreshTimer=null;\n if(activeTab()!=='peers' || selectedHash!==hash) return;\n reverseDnsRefreshInFlight=true;\n try{ await loadDetails('peers',{silent:true}); }\n finally{ reverseDnsRefreshInFlight=false; }\n }, REVERSE_DNS_REFRESH_SECONDS*1000);\n }\n function renderPeers(peers){\n const headers=['Flag','IP'];\n if(reverseDnsEnabled) headers.push('Host');\n headers.push('Country','City','Client','%','DL','UL','Port','Flags');\n const rows=(peers||[]).map(p=>{\n const row=[flag(p.country_iso),`<span class=\"peer-ip\">${esc(p.ip)}<a class=\"peer-ip-link\" href=\"https://ipinfo.io/${encodeURIComponent(p.ip||'')}\" target=\"_blank\" rel=\"noopener noreferrer\" title=\"Open IP info\"><i class=\"fa-solid fa-link\"></i></a></span>`];\n if(reverseDnsEnabled) row.push(peerHostCell(p));\n row.push(esc(p.country),esc(p.city),esc(p.client),progressBar(p.completed,'peer-progress peer-progress-wide'),esc(p.down_rate_h),esc(p.up_rate_h),esc(p.port),peerBadges(p));\n return row;\n });\n $('detailPane').innerHTML=responsiveTable(headers,rows,reverseDnsEnabled ? 'peers-table peers-table-hosts' : 'peers-table');\n scheduleReverseDnsPeerRefresh(peers);\n }\n";
|
export const torrentPeerDetailsSource = " function peerBadges(p){\n const badges=[];\n if(p.encrypted) badges.push('<span class=\"badge text-bg-success\">enc</span>');\n if(p.incoming) badges.push('<span class=\"badge text-bg-info\">in</span>');\n if(p.snubbed) badges.push('<span class=\"badge text-bg-warning\">snub</span>');\n if(p.banned) badges.push('<span class=\"badge text-bg-danger\">ban</span>');\n return badges.join(' ') || '<span class=\"text-muted\">-</span>';\n }\n function peerHostCell(p){\n const host=String(p.host||'').trim();\n // Note: Hostnames use the available peer-table space instead of a fixed character-style cap.\n if(host) return `<span class=\"peer-host\" title=\"${esc(host)}\">${esc(host)}</span>`;\n if(p.host_pending) return '<span class=\"text-muted\">resolving</span>';\n return '<span class=\"text-muted\">-</span>';\n }\n function hasPendingReverseDns(peers){\n return reverseDnsEnabled && (peers||[]).some(p=>p && p.host_pending);\n }\n function clearReverseDnsPeerRefresh(){\n clearTimeout(reverseDnsRefreshTimer);\n reverseDnsRefreshTimer=null;\n reverseDnsRefreshInFlight=false;\n reverseDnsRefreshAttempts=0;\n reverseDnsRefreshHash=null;\n }\n function scheduleReverseDnsPeerRefresh(peers){\n // Note: PTR results are checked on a short independent loop, not on the manual/auto peers refresh interval.\n if(!hasPendingReverseDns(peers)){ clearReverseDnsPeerRefresh(); return; }\n if(activeTab()!=='peers' || !selectedHash) return;\n const hash=selectedHash;\n if(reverseDnsRefreshHash!==hash){ reverseDnsRefreshHash=hash; reverseDnsRefreshAttempts=0; }\n if(reverseDnsRefreshTimer || reverseDnsRefreshAttempts>=REVERSE_DNS_REFRESH_MAX_ATTEMPTS) return;\n reverseDnsRefreshAttempts+=1;\n reverseDnsRefreshTimer=setTimeout(async()=>{\n reverseDnsRefreshTimer=null;\n if(activeTab()!=='peers' || selectedHash!==hash) return;\n reverseDnsRefreshInFlight=true;\n try{ await loadDetails('peers',{silent:true}); }\n finally{ reverseDnsRefreshInFlight=false; }\n }, REVERSE_DNS_REFRESH_SECONDS*1000);\n }\n function renderPeers(peers, activity=null){\n const headers=['Flag','IP'];\n if(reverseDnsEnabled) headers.push('Host');\n headers.push('Country','City','Client','%','DL','UL','Port','Flags');\n const rows=(peers||[]).map(p=>{\n const row=[flag(p.country_iso),`<span class=\"peer-ip\">${esc(p.ip)}<a class=\"peer-ip-link\" href=\"https://ipinfo.io/${encodeURIComponent(p.ip||'')}\" target=\"_blank\" rel=\"noopener noreferrer\" title=\"Open IP info\"><i class=\"fa-solid fa-link\"></i></a></span>`];\n if(reverseDnsEnabled) row.push(peerHostCell(p));\n row.push(esc(p.country),esc(p.city),esc(p.client),progressBar(p.completed,'peer-progress peer-progress-wide'),esc(p.down_rate_h),esc(p.up_rate_h),esc(p.port),peerBadges(p));\n return row;\n });\n const activeEmpty = !(peers||[]).length && peerActivityLooksActive(activity);\n const note = activeEmpty ? '<div class=\"peer-refresh-note\"><i class=\"fa-solid fa-arrows-rotate\"></i> rTorrent reports active transfer; peer rows will refresh in the background.</div>' : '';\n $('detailPane').innerHTML=note+responsiveTable(headers,rows,reverseDnsEnabled ? 'peers-table peers-table-hosts' : 'peers-table');\n schedulePeerEmptyRetry(peers, activity);\n scheduleReverseDnsPeerRefresh(peers);\n }\n";
|
||||||
|
|||||||
@@ -2597,6 +2597,15 @@ body.mobile-mode .mobile-filter-bar {
|
|||||||
color: var(--bs-primary);
|
color: var(--bs-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.peer-refresh-note {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: var(--bs-secondary-color);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
.auth-page {
|
.auth-page {
|
||||||
display: grid;
|
display: grid;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
|||||||
Reference in New Issue
Block a user