adding availability
This commit is contained in:
2
main.go
2
main.go
@@ -162,7 +162,7 @@ var torrentFields = []string{
|
|||||||
"id", "name", "totalSize", "downloadedEver", "uploadedEver", "uploadRatio",
|
"id", "name", "totalSize", "downloadedEver", "uploadedEver", "uploadRatio",
|
||||||
"percentDone", "status", "eta", "error", "errorString", "doneDate", "isFinished",
|
"percentDone", "status", "eta", "error", "errorString", "doneDate", "isFinished",
|
||||||
"rateDownload", "rateUpload", "peersConnected", "peersGettingFromUs", "peersSendingToUs",
|
"rateDownload", "rateUpload", "peersConnected", "peersGettingFromUs", "peersSendingToUs",
|
||||||
"peers",
|
"availability", "peers",
|
||||||
}
|
}
|
||||||
|
|
||||||
type rpcRequest struct {
|
type rpcRequest struct {
|
||||||
|
|||||||
@@ -56,13 +56,15 @@ Torrent list requests should include:
|
|||||||
id, name, totalSize, downloadedEver, uploadedEver, uploadRatio,
|
id, name, totalSize, downloadedEver, uploadedEver, uploadRatio,
|
||||||
percentDone, status, eta, error, errorString, doneDate, isFinished,
|
percentDone, status, eta, error, errorString, doneDate, isFinished,
|
||||||
rateDownload, rateUpload, peersConnected, peersGettingFromUs, peersSendingToUs,
|
rateDownload, rateUpload, peersConnected, peersGettingFromUs, peersSendingToUs,
|
||||||
peers
|
availability, peers
|
||||||
```
|
```
|
||||||
|
|
||||||
Peer objects are expected to include `address`, `clientName`, `progress`, `isDownloadingFrom`, `isUploadingTo`, `rateToClient`, and `rateToPeer` when available.
|
Peer objects are expected to include `address`, `clientName`, `progress`, `isDownloadingFrom`, `isUploadingTo`, `rateToClient`, and `rateToPeer` when available.
|
||||||
|
|
||||||
The UI definition of "peers at 100%" is an absolute count of `peers[]` entries whose `progress === 1.0`. Do not reinterpret it as a percentage of connected peers.
|
The UI definition of "peers at 100%" is an absolute count of `peers[]` entries whose `progress === 1.0`. Do not reinterpret it as a percentage of connected peers.
|
||||||
|
|
||||||
|
The UI availability value comes from Transmission's `availability` per-piece array. Render and sort it as a complete-copy estimate, treating `-1` entries as pieces available locally.
|
||||||
|
|
||||||
## Frontend Architecture
|
## Frontend Architecture
|
||||||
|
|
||||||
The frontend is a single page with module-level state in `web/app.js`. It polls on a user-selectable interval, defaults to 3 seconds, and fetches torrents and stats in parallel.
|
The frontend is a single page with module-level state in `web/app.js`. It polls on a user-selectable interval, defaults to 3 seconds, and fetches torrents and stats in parallel.
|
||||||
@@ -84,7 +86,7 @@ Current sort keys:
|
|||||||
- `ratio`
|
- `ratio`
|
||||||
- `size`
|
- `size`
|
||||||
- `speed`: orders by combined download and upload speed descending, then download speed, upload speed, and name.
|
- `speed`: orders by combined download and upload speed descending, then download speed, upload speed, and name.
|
||||||
- `peer100`: orders by peers at 100% descending, then connected peers descending, progress descending, and name ascending.
|
- `availability`: orders by computed availability descending, then progress, then name.
|
||||||
|
|
||||||
## UI And Design Rules
|
## UI And Design Rules
|
||||||
|
|
||||||
|
|||||||
59
web/app.js
59
web/app.js
@@ -250,9 +250,50 @@ function activitySpeed(t) {
|
|||||||
return (t.rateDownload || 0) + (t.rateUpload || 0);
|
return (t.rateDownload || 0) + (t.rateUpload || 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function availabilityValue(t) {
|
||||||
|
if (typeof t.availability === "number" && Number.isFinite(t.availability)) {
|
||||||
|
return t.availability;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pieces = Array.isArray(t.availability) ? t.availability : [];
|
||||||
|
if (!pieces.length) return null;
|
||||||
|
|
||||||
|
const counts = [];
|
||||||
|
let min = Infinity;
|
||||||
|
for (const piece of pieces) {
|
||||||
|
const raw = Number(piece);
|
||||||
|
if (!Number.isFinite(raw)) continue;
|
||||||
|
const count = raw < 0 ? 1 : raw;
|
||||||
|
counts.push(count);
|
||||||
|
if (count < min) min = count;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!counts.length) return null;
|
||||||
|
|
||||||
|
let aboveMin = 0;
|
||||||
|
for (const count of counts) {
|
||||||
|
if (count > min) aboveMin += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return min + (aboveMin / counts.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
function availabilitySortValue(t) {
|
||||||
|
const value = availabilityValue(t);
|
||||||
|
return value == null ? -1 : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function humanAvailability(value) {
|
||||||
|
if (value == null || !Number.isFinite(value) || value < 0) return "—";
|
||||||
|
return `${value.toFixed(value < 10 ? 2 : 1)}x`;
|
||||||
|
}
|
||||||
|
|
||||||
function sortTorrents(list) {
|
function sortTorrents(list) {
|
||||||
const key = state.sortKey;
|
const key = state.sortKey;
|
||||||
const sorted = list.slice();
|
const sorted = list.slice();
|
||||||
|
const availabilityValues = key === "availability"
|
||||||
|
? new Map(sorted.map((t) => [t, availabilitySortValue(t)]))
|
||||||
|
: null;
|
||||||
sorted.sort((a, b) => {
|
sorted.sort((a, b) => {
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case "name":
|
case "name":
|
||||||
@@ -268,14 +309,10 @@ function sortTorrents(list) {
|
|||||||
|| ((b.rateDownload || 0) - (a.rateDownload || 0))
|
|| ((b.rateDownload || 0) - (a.rateDownload || 0))
|
||||||
|| ((b.rateUpload || 0) - (a.rateUpload || 0))
|
|| ((b.rateUpload || 0) - (a.rateUpload || 0))
|
||||||
|| compareName(a, b);
|
|| compareName(a, b);
|
||||||
case "peer100": {
|
case "availability":
|
||||||
const pa = peerStatus(a);
|
return (availabilityValues.get(b) - availabilityValues.get(a))
|
||||||
const pb = peerStatus(b);
|
|
||||||
return (pb.at100 - pa.at100)
|
|
||||||
|| (pb.connected - pa.connected)
|
|
||||||
|| ((b.percentDone || 0) - (a.percentDone || 0))
|
|| ((b.percentDone || 0) - (a.percentDone || 0))
|
||||||
|| compareName(a, b);
|
|| compareName(a, b);
|
||||||
}
|
|
||||||
case "status": {
|
case "status": {
|
||||||
const ra = STATUS_RANK[classify(a).key] ?? 99;
|
const ra = STATUS_RANK[classify(a).key] ?? 99;
|
||||||
const rb = STATUS_RANK[classify(b).key] ?? 99;
|
const rb = STATUS_RANK[classify(b).key] ?? 99;
|
||||||
@@ -359,6 +396,10 @@ function createCard() {
|
|||||||
<span class="k">ETA</span>
|
<span class="k">ETA</span>
|
||||||
<span class="v" data-field="eta"></span>
|
<span class="v" data-field="eta"></span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="stat-cell">
|
||||||
|
<span class="k">Availability</span>
|
||||||
|
<span class="v mono" data-field="availability"></span>
|
||||||
|
</div>
|
||||||
<div class="stat-cell">
|
<div class="stat-cell">
|
||||||
<span class="k">Peers @ 100%</span>
|
<span class="k">Peers @ 100%</span>
|
||||||
<span class="v mono" data-field="peers-100"></span>
|
<span class="v mono" data-field="peers-100"></span>
|
||||||
@@ -397,6 +438,7 @@ function updateCard(card, t) {
|
|||||||
const pctText = (pct * 100).toFixed(pct === 1 ? 0 : 1) + "%";
|
const pctText = (pct * 100).toFixed(pct === 1 ? 0 : 1) + "%";
|
||||||
const isDone = pct >= 1.0;
|
const isDone = pct >= 1.0;
|
||||||
const ps = peerStatus(t);
|
const ps = peerStatus(t);
|
||||||
|
const availability = availabilityValue(t);
|
||||||
const dl = t.rateDownload || 0;
|
const dl = t.rateDownload || 0;
|
||||||
const ul = t.rateUpload || 0;
|
const ul = t.rateUpload || 0;
|
||||||
const eta = (dl > 0 && !isDone) ? formatETA(t.eta) : (isDone ? "—" : "∞");
|
const eta = (dl > 0 && !isDone) ? formatETA(t.eta) : (isDone ? "—" : "∞");
|
||||||
@@ -444,6 +486,11 @@ function updateCard(card, t) {
|
|||||||
setValueClass(ulEl, ul > 0 ? "success" : "dim");
|
setValueClass(ulEl, ul > 0 ? "success" : "dim");
|
||||||
|
|
||||||
setText(field(card, "eta"), eta);
|
setText(field(card, "eta"), eta);
|
||||||
|
|
||||||
|
const availabilityEl = field(card, "availability");
|
||||||
|
setText(availabilityEl, humanAvailability(availability));
|
||||||
|
setValueClass(availabilityEl, availability == null ? "dim" : availability >= 1 ? "success" : "warn");
|
||||||
|
|
||||||
setText(field(card, "peers-100"), `${ps.at100}/${ps.connected}`);
|
setText(field(card, "peers-100"), `${ps.at100}/${ps.connected}`);
|
||||||
setText(field(card, "finished"), formatDate(t.doneDate));
|
setText(field(card, "finished"), formatDate(t.doneDate));
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
<button class="sort-btn" data-sort="ratio">Ratio</button>
|
<button class="sort-btn" data-sort="ratio">Ratio</button>
|
||||||
<button class="sort-btn" data-sort="size">Size</button>
|
<button class="sort-btn" data-sort="size">Size</button>
|
||||||
<button class="sort-btn" data-sort="speed">Speed</button>
|
<button class="sort-btn" data-sort="speed">Speed</button>
|
||||||
<button class="sort-btn" data-sort="peer100">100% Peers</button>
|
<button class="sort-btn" data-sort="availability">Availability</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar-actions">
|
<div class="toolbar-actions">
|
||||||
<label class="interval-control" for="refresh-interval">
|
<label class="interval-control" for="refresh-interval">
|
||||||
|
|||||||
Reference in New Issue
Block a user