advanced torrents web panel

This commit is contained in:
orignal
2026-10-17 09:38:24 -04:00
parent 7ab983ace9
commit 84b019ded8
4 changed files with 1123 additions and 397 deletions
Regular → Executable
+138 -306
View File
@@ -1,314 +1,146 @@
<script src="torrent-rpc.js"></script>
<script>window.tjs = new TorrentClient();</script>
<style>
/*todo: more 2000 design*/
#torrent-ui button {
border-radius: 6.66px;
padding: 0.1vw;
margin: 2px;
text-decoration: underline;
color: black;
background: aquamarine;
text-transform: uppercase;
.progress-cell {
position: relative;
padding: 0;
height: 18px;
min-width: 110px;
}
#torrent-ui button:hover {
cursor:pointer;
background-color: yellow;
transition: background-color 0.5s ease 0.1s;
}
#torrent-ui {
display: flex;
flex-direction: column;
gap: 2.5px;
}
#torrents li {
border-bottom: 2px solid #333333;
}
.progress-bar {
position: absolute;
inset: 0;
background: #e5e7eb;
}
.progress-fill {
height: 100%;
}
.progress-text {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
color: #111;
line-height: 1;
}
.compact-table {
border-collapse: collapse;
width: 100%;
}
.compact-table td,
.compact-table th {
border: 1px solid black;
padding: 0 4px;
height: 18px;
line-height: 1.3;
overflow-wrap: anywhere;
box-sizing: border-box;
}
#fs-error fieldset {
padding: 0;
margin: 0;
}
#fs-error h2,
#fs-error h3 {
margin-top: 10;
margin-bottom: 10;
margin-left: 10;
}
td.buttons-cell {
padding: 0;
max-width: 100px;
}
.buttons-cell .button-row {
display: flex;
flex-direction: row;
width: 100%;
height: 100%;
gap: 0;
}
.button-column {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
gap: 5px;
}
.buttons-cell button,
.buttons-column button,
.button-row button {
flex: 1;
margin: 0;
border: 1px;
border-radius: 0;
min-width: 0;
line-height: 1;
}
.button-row button:hover {
background-color: orange;
}
.fs-buttons {
width: 100%;
max-width: 100px;
box-sizing: border-box;
}
.container {
display: flex;
}
fieldset {
flex: 1;
min-width: 0;
}
</style>
<fieldset>
<legend>Torrent Client</legend>
<div id="torrent-ui">
<label for='torrent_manet_url'>Your a torrent magnet link (only magnet): <input id='torrent_manet_url' type='text' id='torrent_manet_url'/><button id='btn_magnet'>download</button>
<h3 id="connection_status">_</h3>
<input type="file" id="torrentf" accept=".torrent" style="display: none" />
<button onclick="document.getElementById('torrentf').click()">
Add torrent
</button>
<button onclick="stopAll()">
stopAll
</button>
<button onclick="startAll()">
startAll
</button>
<ul id="torrents"></ul>
</div>
</fieldset>
<div id="torrent_details_modal" style="display: none">
<h3 id="modal_torrent_title"></h3>
<p style="color:#303030" id="modal_torrent_hash"></p>
<h4>Files:</h4>
<ul id="modal_files_list"></ul>
<h4>Peers (<span id="peer_count">0</span>):</h4>
<ul id="modal_peers_list"></ul>
<h4>Trackers (<span id="trackers_count">0</span>):</h4>
<ul id="modal_trackers_list"></ul>
<h4>trackerStats:</h4>
<pre id="modal_tracker_stats"></pre>
<button onclick="closeDetailsModal()">Close</button>
</div>
<script src="./rpc.js"></script>
<script>
let currentTorrentsData = [];
async function stopAll()
{
const conf = confirm("Are sure to stop all torrents?");
// console.log(conf)
if(conf && window.tjs)
{
const r = await window.tjs.getTorrents()
//console.log(`torrents: ${r}`)
if (!r?.result?.torrents) return;
r.arguments.torrents.forEach(async (t) => {
console.log(`stop torrent => ${t.id}`)
await window.tjs.stopTorrent([t.id]);
});
}
//if (window.tjs && conf) await window.tjs.stopTorrent();
}
async function startAll()
{
const conf = confirm("Are sure to start all torrents?");
if(conf && window.tjs)
{
const r = await window.tjs.getTorrents()
//console.log(`torrents: ${r}`)
if (!r?.result?.torrents) return;
r.arguments.torrents.forEach( async (t) =>{
console.log(`start torrent => ${t.id}`)
await window.tjs.startTorrent([t.id]);
});
}
//if (window.tjs && conf) await window.tjs.startTorrent();
}
setInterval(async () => {
const statusIndicator = document.getElementById("connection_status");
if (!window.tjs)
return (statusIndicator.textContent = "STATUS: TJS_NOT_LOADED");
try {
const r = await window.tjs.getTorrents();
console.log(r)
if (!r?.result == "success")
return (statusIndicator.textContent = "STATUS: INVALID_RESPONSE");
statusIndicator.textContent = "STATUS: Connected";
statusIndicator.style.color = "rgb(77, 91, 83)";
const torrents = document.getElementById("torrents");
currentTorrentsData = r.arguments.torrents;
if (currentTorrentsData.length === 0) {
torrents.innerHTML = "<li>No downloads</li>";
return;
}
torrents.innerHTML = "";
currentTorrentsData.forEach((el) => {
const percent = el.percentDone
? (el.percentDone * 100).toFixed(2)
: "0.00";
const speed = window.tjs.formatBytes
? window.tjs.formatBytes(el.rateDownload)
: `${el.rateDownload} B/s`;
const speedUp = window.tjs.formatBytes
? window.tjs.formatBytes(el.rateUpload)
: `${el.rateUpload} B/s`;
const status = window.tjs.getStatusString
? window.tjs.getStatusString(el.status)
: el.status;
const totalSize = el.totalSize || 0; // bytes
const ETA = el.rateDownload
? (
(totalSize * (1 - el.percentDone)) /
el.rateDownload /
60
).toFixed(1) + " min"
: "-";
const name = el.name || "|unknown name, maybe a bad torrent|";
const li = document.createElement("li");
const span = document.createElement("span");
span.textContent = `${name} — ${percent}% [${status}] (⬇ ${speed} | ⬆ ${speedUp}) ---- size = ${(totalSize / 1024 / 1024).toFixed(2)}mb; ETA: ${ETA} `;
li.appendChild(span);
const butDetails = document.createElement("button");
butDetails.textContent = "Details";
butDetails.onclick = () => openTorrentDetails(el.id);
li.appendChild(butDetails);
const butStop = document.createElement("button");
butStop.textContent = "Stop";
butStop.onclick = async () => {
if (window.tjs) await window.tjs.stopTorrent([el.id]);
};
li.appendChild(butStop);
const butStart = document.createElement("button");
butStart.textContent = "Start";
butStart.onclick = async () => {
if (window.tjs) await window.tjs.startTorrent([el.id]);
};
li.appendChild(butStart);
const butRemove = document.createElement("button");
butRemove.textContent = "✕";
butRemove.onclick = () => removeTorrent(el.id);
li.appendChild(butRemove);
torrents.appendChild(li);
});
} catch (err) {
console.log(err)
statusIndicator.textContent =
"STATUS: ERROR_WITH_DATA (f12, check your console and write issue)";
statusIndicator.style.color = "#ff3333";
console.log(err)
}
}, 1000);
document.getElementById("torrentf").onchange = (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = async () => {
const bytes = new Uint8Array(reader.result);
let binary = "";
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
const b64 = btoa(binary);
if (window.tjs) await window.tjs.addTorrent(b64);
e.target.value = "";
};
reader.readAsArrayBuffer(file);
};
window.removeTorrent = async (id) => {
const confirmed = confirm(`Drop the torrent with id ${id}?`);
if (confirmed && window.tjs) await window.tjs.removeTorrent(Number(id));
};
window.openTorrentDetails = (id) => {
const torrent = currentTorrentsData.find(
(t) => String(t.id) === String(id),
);
if (!torrent) return;
document.getElementById("modal_torrent_title").textContent =
`DETAILS: ${torrent.name} (#${torrent.id})`;
document.getElementById("modal_torrent_hash").textContent =
`InfoHash: ${torrent.hashString}`;
const filesList = document.getElementById("modal_files_list");
filesList.innerHTML = "";
if (torrent.files?.length) {
torrent.files.forEach((f) => {
const li = document.createElement("li");
const completedMB = (f.bytesCompleted / 1048576).toFixed(2);
const totalMB = (f.length / 1048576).toFixed(2);
const percent = ((f.bytesCompleted / f.length) * 100 || 0).toFixed(1);
li.textContent = `${f.name} — ${completedMB} / ${totalMB} MB (${percent}%)`;
filesList.appendChild(li);
});
} else {
const li = document.createElement("li");
li.textContent = "NO_FILES";
filesList.appendChild(li);
}
const peersList = document.getElementById("modal_peers_list");
const trackersList = document.getElementById("modal_trackers_list");
document.getElementById("peer_count").textContent = torrent.peers
? torrent.peers.length
: 0;
peersList.innerHTML = "";
if (torrent.peers?.length) {
peersList.append(
...torrent.peers.map((p) => {
const li = document.createElement("li");
const downSpeed = (p.rateToClient / 1024).toFixed(2);
const upSpeed = (p.rateToPeer / 1024).toFixed(2);
const client = p.clientName || "Unk";
li.textContent = `${p.address} (${client}) — ${downSpeed} KB/s — ${upSpeed} KB/s`;
return li;
}),
);
} else {
const li = document.createElement("li");
li.textContent = "NO_PEERS";
peersList.appendChild(li);
}
trackersList.innerHTML = "";
if (torrent.trackers?.length) {
trackersList.append(
...torrent.trackers.map((p) => {
const li = document.createElement("li");
li.textContent = `[id:${p.id}] announce: ${p.announce} ; tier - ${p.tier}`;
return li;
}),
);
} else {
const li = document.createElement("li");
li.textContent = "NO_TRACKERS";
trackersList.appendChild(li);
}
document.getElementById("trackers_count").textContent = torrent.trackers
? torrent.trackers.length
: 0;
const modal_TrackerStats = document.getElementById("modal_tracker_stats");
const tstats = torrent.trackerStats;
/*
To convert a Unix timestamp to a human-readable date in JavaScript, multiply the timestamp by 1000 and pass it into the new Date() constructor. JavaScript requires milliseconds, while standard Unix timestamps are measured in second
(so, for js we need ms)
*/
modal_TrackerStats.textContent = tstats
? tstats
.map(
(tr) => {
const leachers = tr.leecherCount >= 0 ? tr.leecherCount : '?'; // TODO: why so big value
return `
Host: ${tr.host}
Announce: ${tr.announce}
Status: ${tr.lastAnnounceResult} (${tr.lastAnnounceSucceeded ? "OK" : "Fail"})
Peers: ${tr.lastAnnouncePeerCount} | Seeders: ${tr.seederCount} | Leechers: ${leachers}
Last Announce: ${new Date(tr.lastAnnounceTime * 1000).toLocaleString()}
Next Announce: ${new Date(tr.nextAnnounceTime * 1000).toLocaleString()}
`}).join("\n" + "-".repeat(40)) : "-";
document.getElementById("torrent_details_modal").style.display = "block";
};
window.closeDetailsModal = () => {
document.getElementById("torrent_details_modal").style.display = "none";
};
const btn_magnet = document.getElementById('btn_magnet')
btn_magnet.onclick = async () => {
const r = await window.tjs.addTorrentByMagnet(document.getElementById('torrent_manet_url').value)
if(!r) alert("Not correct magnet link");
}
window.rpc = new TorrentClient();
</script>
<div id="fs-error">
<fieldset>
<h2 style="color: #ff3333;" id="err-string">failed to load main.js</h2>
<h3 id="err-details"></h3>
</fieldset>
</div>
<div class="container">
<fieldset class="fs-buttons">
<legend>Actions</legend>
<div class="button-column">
<input type="file" id="torrentf" accept=".torrent" style="display: none" />
<button title="Open torrent file" onclick="document.getElementById('torrentf').click()">📂</button>
<button title="Add magnet" onclick="addMagnet()">🧲</button>
</div>
</fieldset>
<fieldset id="fs-session-stats">
<legend>Session stats:</legend>
<div id="session-stats-list"></div>
</fieldset>
<fieldset id="fs-dht-stats" style="display: none">
<legend>DHT stats:</legend>
<div id="dht-stats-list"></div>
</fieldset>
</div>
<div class="container" id="stats"></div>
<div id="torrents"></div>
<div id="details"></div>
<script src="./main.js"></script>
+903
View File
@@ -0,0 +1,903 @@
let torrents_data = []; // torrents table data
let details_timer = null;
const el_torrents = document.getElementById("torrents")
const el_fs_error = document.getElementById("fs-error")
const el_err_string = document.getElementById("err-string");
const el_err_details = document.getElementById("err-details")
const el_details = document.getElementById("details")
const el_torrentf = document.getElementById("torrentf")
const el_dht_stats_list = document.getElementById("dht-stats-list")
const el_fs_dht_stats = document.getElementById("fs-dht-stats");
const el_session_stats_list = document.getElementById("session-stats-list")
const el_fs_session_stats = document.getElementById("fs-session-stats")
refreshTable();
setInterval(async () => refreshTable(), 5000);
refreshSessionStats();
setInterval(async () => refreshSessionStats(), 10000);
refreshDhtStats();
setInterval(async () => refreshDhtStats(), 60000);
function setError(err, err_details) {
el_err_string.textContent = err || "";
el_err_details.textContent = err_details || "";
el_fs_error.style.display = "block";
if (err_details)
console.log(err_details)
}
function clearError() {
this.setError("", "")
el_fs_error.style.display = "none";
}
function startDetailsTimer(id) {
stopDetailsTimer();
details_timer = setInterval(async () => {
await openTorrentDetails([id]);
}, 2500);
}
function stopDetailsTimer() {
clearInterval(details_timer);
details_timer = null;
}
function formatBytes(bytesPerSec) {
if (!Number.isFinite(bytesPerSec) || bytesPerSec <= 0) {
return "0 B/s";
}
const units = ["B/s", "KB/s", "MB/s", "GB/s", "TB/s"];
const i = Math.min(
Math.floor(Math.log(bytesPerSec) / Math.log(1024)),
units.length - 1
);
return `${(bytesPerSec / 1024 ** i).toFixed(2)} ${units[i]}`;
}
function formatETA(tr_obj) {
const speed = tr_obj.rateDownload || 0;
const remaining = (tr_obj.totalSize || 0) * (1 - (tr_obj.percentDone || 0));
if (speed <= 0) return "";
const seconds = remaining / speed;
if (seconds < 60) {
return `${Math.ceil(seconds)} sec`;
}
if (seconds < 3600) {
return `${(seconds / 60).toFixed(1)} min`;
}
if (seconds < 86400) {
return `${(seconds / 3600).toFixed(1)} h`;
}
return `${(seconds / 86400).toFixed(1)} days`;
}
function formatSize(bytes) {
if (!bytes) return "0 B";
const units = ["B", "KB", "MB", "GB", "TB"];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${units[i]}`;
}
async function addMagnet() {
const name = prompt("enter the magnet link:", "");
if (name !== null) {
const r = await window.rpc.addTorrentByMagnet(name)
if (r.result !== "success")
alert(r.result);
}
}
async function stopAll() {
if (!window.rpc)
return;
const r = await window.rpc.torrentStop()
if (r.result !== "success")
alert(r.result);
}
async function startAll() {
if (!window.rpc)
return;
const r = await window.rpc.torrentStart()
if (r.result !== "success")
alert(r.result);
}
window.removeTorrent = async (id) => {
if (!window.rpc)
return;
const confirmed = confirm(`remove the torrent with id=${id} ?`);
if (confirmed)
await window.rpc.removeTorrent(Number(id), false);
};
window.removeTorrentAndFiles = async (id) => {
if (!window.rpc)
return;
const confirmed = confirm(`remove the torrent AND FILES with id=${id} ?`);
if (confirmed)
await window.rpc.removeTorrent(Number(id), true);
};
// buttons in torrents table
torrents.addEventListener("click", async (event) => {
if (!window.rpc)
return;
// action buttons for torrents
const torrentActions = {
".tr-details": async (id) => openTorrentDetails([id]),
".tr-start": async (id) => window.rpc.torrentStart([id]),
".tr-pause": async (id) => window.rpc.torrentStop([id]),
".tr-delete": async (id) => removeTorrent([id]),
".tr-deletefiles": async (id) => removeTorrentAndFiles([id]),
".tr-reannounce": async (id) => window.rpc.torrentReannounce([id]),
".tr-start-all": async () => startAll(),
".tr-pause-all": async () => stopAll(),
".tr-reannounce-all": async (id) => window.rpc.torrentReannounce()
};
const button = event.target.closest(
Object.keys(torrentActions).join(",")
);
if (!button || !torrents.contains(button))
return;
const action = Object.keys(torrentActions).find(
(selector) => button.matches(selector)
);
if (!action)
return;
const id = Number(button.dataset.id || 0);
if (!Number.isSafeInteger(id))
return;
await torrentActions[action](id);
await refreshTable();
});
async function refreshSessionStats() {
if (!window.rpc)
return;
const r = await window.rpc.sessionStats();
if (!r)
return (el_fs_session_stats.style.display = "none")
session_data = r.arguments["current-stats"]
//console.log('session', session_data)
const session_list = document.createElement("div");
const session_fields = ["downloadedBytes", "uploadedBytes"];
Object.keys(session_data).forEach(key => {
const cell = document.createElement("li");
const value = session_data[key];
switch (key) {
case "downloadedBytes":
cell.textContent = `Downloaded: ${formatSize(value)}`;
break;
case "uploadedBytes":
cell.textContent = `Uploaded: ${formatSize(value)}`;
break;
}
if (cell.textContent)
session_list.appendChild(cell);
});
el_session_stats_list.replaceChildren(session_list)
el_fs_session_stats.style.display = "block";
}
async function refreshDhtStats() {
if (!window.rpc)
return;
const r = await window.rpc.dhtStats();
if (!r)
return (el_fs_dht_stats.style.display = "none")
dht_data = r.arguments
//console.log('dht', dht_data)
const dht_list = document.createElement("div");
const dht_fields = ["supported", "numNodes", "numBuckets", "numTorrents", "numHeardFrom", "numFailedPeers"];
Object.keys(dht_data).forEach(key => {
const cell = document.createElement("li");
const value = dht_data[key];
switch (key) {
case "supported":
cell.textContent = `Enabled: ${value}`;
break;
case "numNodes":
cell.textContent = `Nodes: ${value}`;
break;
case "numBuckets":
cell.textContent = `Buckets: ${value}`;
break;
case "numTorrents":
cell.textContent = `Torrents: ${value}`;
break;
case "numHeardFrom":
cell.textContent = `Heard from: ${value}`;
break;
case "numFailedPeers":
cell.textContent = `Failed peers: ${value}`;
break;
}
if (cell.textContent)
dht_list.appendChild(cell);
});
el_dht_stats_list.replaceChildren(dht_list)
el_fs_dht_stats.style.display = "block";
}
// refresh table with torrents
async function refreshTable() {
if (!window.rpc)
return setError('failed to load rpc.js', "")
try {
const r = await window.rpc.torrentGet();
if (r?.result !== "success")
return setError('failed response', r)
torrents_data = r.arguments.torrents;
if (torrents_data.length === 0) {
el_torrents.style.display = "none";
el_torrents.replaceChildren();
clearError();
return;
}
const table_cols = ["id", "name", "progress", "S / L", "speedDw", "speedUp", "size", "have", "eta", "actions"];
const table = document.createElement("table");
table.className = "compact-table";
table_cols.forEach((item) => {
const th = document.createElement("th");
th.textContent = item;
table.appendChild(th);
});
let allDw = allUp = allSize = 0;
torrents_data.forEach(tr_obj => {
const tr = document.createElement("tr");
table_cols.forEach(key => {
const cell = document.createElement("td");
const value = tr_obj[key];
switch (key) {
case "id":
cell.textContent = value ?? -1;
break;
case "name":
cell.textContent = value ?? "null";
if (tr_obj.errorString) {
cell.style.fontWeight = "bold";
cell.style.color = "#ff3333";
}
break;
case "progress": {
cell.className = "progress-cell";
const percent = Math.max(0, Math.min(100, (tr_obj.percentDone * 100).toFixed(1)))
const progressBar = document.createElement("div");
progressBar.className = "progress-bar";
const progressFill = document.createElement("div");
progressFill.className = "progress-fill";
progressFill.style.width = `${percent}%`;
const is_magnet = tr_obj.metadataPercentComplete < 1
pr_color = "#E5E7EB"
pr_lable = is_magnet ? '🧲' : `${percent}%`;
switch (tr_obj.status) {
case 4:
pr_color = "#44abff"; // downloading
if (!tr_obj.rateDownload && !is_magnet) {
pr_color = "#0072cf"; // stalled
pr_lable += ', stalled'
}
break;
case 6:
pr_color = "#00FF5A"; // seeding
break;
case 0:
if (tr_obj.percentDone < 1) {
pr_color = "#ffd000"; // paused
pr_lable += ', paused'
}
}
progressFill.style.backgroundColor = pr_color;
progressBar.appendChild(progressFill);
const progressText = document.createElement("div");
progressText.className = "progress-text";
progressText.textContent = pr_lable
cell.append(progressBar, progressText);
break;
}
case "S / L":
seeders = leechers = 0
tr_obj.peers.forEach(peer => {
if (peer.progress !== 0)
peer.progress === 1 ? seeders++ : leechers++
});
if (tr_obj.status === 6)
cell.textContent = leechers ? `${leechers}` : "" // we are seeding
else if (seeders || leechers)
cell.textContent = `${seeders} / ${leechers}`; // we are leeching
break;
case "speedDw":
cell.textContent = tr_obj.rateDownload ? formatBytes(tr_obj.rateDownload) : "";
allDw += tr_obj.rateDownload;
break;
case "speedUp":
cell.textContent = tr_obj.rateUpload ? formatBytes(tr_obj.rateUpload) : "";
allUp += tr_obj.rateUpload;
break;
case "size":
cell.textContent = tr_obj.totalSize ? formatSize(tr_obj.totalSize) : "";
allSize += tr_obj.totalSize
break;
case "have":
cell.textContent = (tr_obj.haveValid === 0 || tr_obj.haveValid === tr_obj.totalSize) ? "" : formatSize(tr_obj.haveValid);
allSize += tr_obj.totalSize
break;
case "eta":
cell.textContent = formatETA(tr_obj);
break;
case "actions":
cell.className = "buttons-cell";
const buttonRow = document.createElement("div");
buttonRow.className = "button-row";
const torrentButtons = [
{ className: "tr-details", text: "ℹ️", title: "Details" },
{ className: "tr-start", text: "▶️", title: "Start" },
{ className: "tr-pause", text: "⏸️", title: "Pause" },
{ className: "tr-reannounce", text: "🔄", title: "Reannounce" },
{ className: "tr-delete", text: "❌", title: "Delete torrent"},
{ className: "tr-deletefiles", text: "🗑️", title: "Delete torrent and files"}
];
torrentButtons.forEach(({className, text, title}) => {
const button = document.createElement("button");
button.className = className;
button.textContent = text;
button.dataset.id = tr_obj.id;
button.title = title;
buttonRow.appendChild(button);
});
cell.appendChild(buttonRow);
break;
}
tr.appendChild(cell);
});
table.appendChild(tr);
});
// bottom row
const tr = document.createElement("tr");
table_cols.forEach(key => {
const cell = document.createElement("td");
switch (key) {
case "speedDw":
cell.textContent = allDw ? formatBytes(allDw) : "";
cell.style.fontWeight = "bold";
break;
case "speedUp":
cell.textContent = allUp ? formatBytes(allUp) : "";
cell.style.fontWeight = "bold";
break;
case "size":
cell.textContent = allSize ? formatSize(allSize) : "";
cell.style.fontWeight = "bold";
break;
case "actions":
cell.className = "buttons-cell";
const buttonRow = document.createElement("div");
buttonRow.className = "button-row";
const globalButtons = [
{ className: "tr-start-all", text: "▶️", title: "Start all" },
{ className: "tr-pause-all", text: "⏸️", title: "Pause all" },
{ className: "tr-reannounce-all", text: "🔄", title: "Reannounce all" },
];
globalButtons.forEach(({className, text, title}) => {
const button = document.createElement("button");
button.className = className;
button.textContent = text;
button.title = title;
buttonRow.appendChild(button);
});
cell.appendChild(buttonRow);
break;
}
tr.appendChild(cell);
});
table.appendChild(tr);
// main frame label
const fs_torrents = document.createElement("fieldset");
const fs_torrents_lable = document.createElement("legend");
fs_torrents_lable.textContent = `Torrents: ${torrents_data.length}`
fs_torrents.appendChild(fs_torrents_lable);
fs_torrents.appendChild(table);
document.title = `↓${formatBytes(allDw)} ↑${formatBytes(allUp)}`;
torrents.replaceChildren(fs_torrents);
clearError()
} catch (err) {
setError('failed to update torrents table', err)
}
}
// add local torrent file
el_torrentf.onchange = (e) => {
if (!window.rpc)
return;
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = async () => {
const bytes = new Uint8Array(reader.result);
let binary = "";
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
const b64 = btoa(binary);
if (window.rpc) await window.rpc.addTorrent(b64);
e.target.value = "";
};
reader.readAsArrayBuffer(file);
};
// close torrent details
window.closeDetailsModal = () => {
el_details.style.display = "none";
stopDetailsTimer();
};
// open torrent details
window.openTorrentDetails = (tr_id) => {
if (!window.rpc)
return;
const torrent = torrents_data.find((t) => t.id == tr_id);
if (!torrent) return;
// main frame
const fs_main = document.createElement("fieldset");
// main frame label
const legend = document.createElement("legend");
const content = document.createElement("div");
const lable = document.createElement("span");
const close_link = document.createElement("href");
lable.textContent = `#${torrent.id}: ${torrent.name} `;
close_link.textContent = "Close";
close_link.href = "#";
close_link.style.color = "#0000ee";
close_link.style.cursor = "pointer";
close_link.style.textDecoration = "underline";
close_link.onclick = closeDetailsModal;
legend.style.width = "100%";
content.style.display = "flex";
content.style.justifyContent = "space-between";
content.style.width = "100%";
content.append(lable, close_link);
legend.appendChild(content);
fs_main.prepend(legend);
const info_list = document.createElement("ul");
const info_cols = ["status", "hashString", "magnetLink", "pieces", "errorString"];
info_cols.forEach(key => {
const cell = document.createElement("li");
const value = torrent[key];
switch (key) {
case "hashString":
cell.textContent = `Hash: ${value || "None"}`;
break;
case "magnetLink":
cell.textContent = `Magnet: `;
const href = value.length > 60 ? value.slice(0, 60) + '...' : url;
const link = document.createElement('a');
link.textContent = href;
link.href = value;
cell.appendChild(link);
break;
case "pieces":
if (torrent.pieceCount) {
cell.textContent = `Pieces: ${torrent.pieceCount} x ${formatSize(torrent.pieceSize)}`;
}
break;
case "status":
tr_status = [
"Stopped",
"QueuedToVerifyLocalData",
"VerifyingLocalData",
"QueuedToDownload",
"Downloading",
"QueuedToSeed",
"Seeding"
];
if (torrent.status < tr_status.length) {
cell.textContent = `Status: ${tr_status[torrent.status]}`;
}
break;
case "errorString":
if (value) {
cell.textContent = `Error: ${value || "None"}`;
cell.style.fontWeight = "bold";
cell.style.color = "#ff3333";
}
break;
}
if (cell.textContent)
info_list.appendChild(cell);
});
fs_main.appendChild(info_list);
// PEERS
if (torrent.peers.length) {
// peers frame
const fs_peers = document.createElement("fieldset");
// peers frame label
const fs_peers_lable = document.createElement("legend");
fs_peers_lable.textContent = `Peers: ${torrent.peers.length}`
fs_peers.appendChild(fs_peers_lable);
// peer fields
const peer_table_cols = ["address", "client", "flags", "progress", "speedDw", "speedUp"];
// peer table
const peer_table = document.createElement("table");
peer_table.className = "compact-table";
// peer table header
peer_table_cols.forEach((item) => {
const th = document.createElement("th");
th.textContent = item;
peer_table.appendChild(th);
});
// peer table data
torrent.peers.forEach(peer => {
const tr = document.createElement("tr");
peer_table_cols.forEach(key => {
const cell = document.createElement("td");
const value = peer[key];
switch (key) {
case "address":
cell.textContent = value;
break;
case "client":
cell.textContent = peer.clientName;
break;
case "flags":
cell.textContent = peer.flagStr;
break;
case "progress":
cell.className = "progress-cell";
const percent = Math.max(0, Math.min(100, (peer.progress * 100).toFixed(1)))
const progressBar = document.createElement("div");
progressBar.className = "progress-bar";
const progressFill = document.createElement("div");
progressFill.className = "progress-fill";
progressFill.style.width = `${percent}%`;
progressFill.style.backgroundColor = peer.progress === 1 ? "#00FF5A" : "#4DAFFF";
progressBar.appendChild(progressFill);
const progressText = document.createElement("div");
progressText.className = "progress-text";
progressText.textContent = `${percent}%`;
cell.append(progressBar, progressText);
break;
case "speedDw":
cell.textContent = peer.rateToClient ? formatBytes(peer.rateToClient) : "";
break;
case "speedUp":
cell.textContent = peer.rateToPeer ? formatBytes(peer.rateToPeer) : "";
break;
}
tr.appendChild(cell);
});
peer_table.appendChild(tr);
});
fs_peers.appendChild(peer_table);
fs_main.appendChild(fs_peers);
} // peers
// TRACKERS
if (torrent.trackerStats.length) {
// trackers frame
const fs_trackers = document.createElement("fieldset");
// trackers frame label
const fs_trackers_lable = document.createElement("legend");
fs_trackers_lable.textContent = `Trackers: ${torrent.trackerStats.length}`
fs_trackers.appendChild(fs_trackers_lable);
// trackers fields
const trackers_table_cols = ["id", "announce", "peers", "seeders", "leechers", "lastAnnounceResult", "lastAnnounceTime", "nextAnnounceTime"];
// trackers table
const trackers_table = document.createElement("table");
trackers_table.className = "compact-table";
// trackers table header
trackers_table_cols.forEach((item) => {
const th = document.createElement("th");
th.textContent = item;
trackers_table.appendChild(th);
});
// peer table data
torrent.trackerStats.forEach(tracker => {
const tr = document.createElement("tr");
trackers_table_cols.forEach(key => {
const cell = document.createElement("td");
const value = tracker[key];
switch (key) {
case "id":
cell.textContent = value;
break;
case "announce":
cell.textContent = value;
break;
case "seeders":
cell.textContent = tracker.seederCount || 0;
break;
case "leechers":
cell.textContent = tracker.leecherCount >= 0 ? tracker.leecherCount : '?'; // todo: big value?
break;
case "peers":
cell.textContent = tracker.lastAnnouncePeerCount || 0;
break;
case "lastAnnounceResult":
cell.textContent = value;
break;
case "lastAnnounceTime":
cell.textContent = `${new Date(tracker.lastAnnounceTime * 1000).toLocaleString()}`;
break;
case "nextAnnounceTime":
cell.textContent = `${new Date(tracker.nextAnnounceTime * 1000).toLocaleString()}`;
break;
}
tr.appendChild(cell);
});
trackers_table.appendChild(tr);
});
fs_trackers.appendChild(trackers_table);
fs_main.appendChild(fs_trackers);
} // trackers
// FILES
if (torrent.files.length) {
// files frame
const fs_files = document.createElement("fieldset");
// files frame label
const fs_files_lable = document.createElement("legend");
fs_files_lable.textContent = `Files: ${torrent.files.length}`
fs_files.appendChild(fs_files_lable);
// files fields
const files_table_cols = ["name", "progress", "size", "have", "left"];
// files table
const files_table = document.createElement("table");
files_table.className = "compact-table";
// files table header
files_table_cols.forEach((item) => {
const th = document.createElement("th");
th.textContent = item;
files_table.appendChild(th);
});
// files table data
torrent.files.forEach(file => {
const tr = document.createElement("tr");
files_table_cols.forEach(key => {
const cell = document.createElement("td");
const value = file[key];
switch (key) {
case "name":
cell.textContent = value;
break;
case "progress":
cell.className = "progress-cell";
const progress = file.length > 0 ?
Math.min(1, Math.max(0, file.bytesCompleted / file.length)) :
0;
const percent = Math.max(0, Math.min(100, (progress * 100).toFixed(1)))
const progressBar = document.createElement("div");
progressBar.className = "progress-bar";
const progressFill = document.createElement("div");
progressFill.className = "progress-fill";
progressFill.style.width = `${percent}%`;
progressFill.style.backgroundColor = progress === 1 ? "#00FF5A" : "#4DAFFF";
progressBar.appendChild(progressFill);
const progressText = document.createElement("div");
progressText.className = "progress-text";
progressText.textContent = `${percent}%`;
cell.append(progressBar, progressText);
break;
case "size":
cell.textContent = formatSize(file.length);
break;
case "have":
cell.textContent = file.bytesCompleted ? formatSize(file.bytesCompleted) : "";
break;
case "left":
cell.textContent = file.length - file.bytesCompleted ? formatSize(file.length - file.bytesCompleted) : "";
break;
}
tr.appendChild(cell);
});
files_table.appendChild(tr);
});
fs_files.appendChild(files_table);
fs_main.appendChild(fs_files);
}
el_details.replaceChildren(fs_main);
el_details.style.display = "block";
startDetailsTimer(tr_id);
};
+82
View File
@@ -0,0 +1,82 @@
class TorrentClient {
endpoint = "../rpc"
torrent_fields = [
"id",
"name",
"status",
"pieceCount",
"pieceSize",
"rateDownload",
"rateUpload",
"totalSize",
"haveValid",
"percentDone",
"files",
"peers",
"trackers",
"trackerStats",
"magnetLink",
"hashString",
"metadataPercentComplete"
]
async request(method, args = {}) {
const res = await fetch(this.endpoint, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
method,
arguments: args
})
});
return res.json();
}
async addTorrentByMagnet(magnet) {
return this.request("torrent-add", { filename: magnet });
}
async addTorrent(b64metainfo) {
return this.request("torrent-add", { metainfo: b64metainfo });
}
async removeTorrent(id, deletefiles = false) {
if (Array.isArray(id) && id.length === 0)
return;
return this.request("torrent-remove", {
ids: Array.isArray(id) ? id : [id],
"delete-local-data": deletefiles
});
}
async torrentGet() {
return this.request("torrent-get", {
fields: this.torrent_fields
});
}
async sessionStats() {
return this.request("session-stats");
}
async dhtStats() {
return this.request("dht-stats");
}
async torrentStop(ids = []) {
return this.request("torrent-stop", ids.length ? { ids } : {});
}
async torrentStart(ids = []) {
return this.request("torrent-start", ids.length ? { ids } : {});
}
async torrentReannounce(ids = []) {
return this.request("torrent-reannounce", ids.length ? { ids } : {});
}
}
-91
View File
@@ -1,91 +0,0 @@
class TorrentClient {
getEndpoint() {
return `../rpc`;
}
getStatusString(status) {
switch (status) {
case 4: return 'DOWNLOADING';
case 6: return 'SEEDING';
case 0: return 'STOPPED';
case 2: return 'CHECKING';
default: return 'QUEUED';
}
}
formatBytes(bytesPerSec) {
if (!bytesPerSec) return '0 B/s';
if (bytesPerSec > 1024 * 1024) {
return (bytesPerSec / (1024 * 1024)).toFixed(2) + ' MB/s';
}
return (bytesPerSec / 1024).toFixed(2) + ' KB/s';
}
async addTorrentByMagnet(magnet)
{
// TODO: check link
const magnetRegex = /^magnet:\?xt=urn:btih:[a-f0-9]{40}(&.+)?$/i;
if(!magnetRegex.test(magnet)) return false;
const res = await fetch(this.getEndpoint(), {
method: "POST",
body: JSON.stringify({ 'method': 'torrent-add', 'arguments': { 'filename': magnet } }),
headers: { 'Content-Type': 'application/json' }
});
return res.json();
}
async addTorrent(b64metainfo) {
const res = await fetch(this.getEndpoint(), {
method: "POST",
body: JSON.stringify({ 'method': 'torrent-add', 'arguments': { 'metainfo': b64metainfo } }),
headers: { 'Content-Type': 'application/json' }
});
return res.json();
}
async removeTorrent(id) {
const res = await fetch(this.getEndpoint(), {
method: "POST",
body: JSON.stringify({ 'method': 'torrent-remove', 'arguments': { 'ids': [id], 'delete-local-data': true } }),
headers: { 'Content-Type': 'application/json' }
});
return res.json();
}
async getTorrents() {
const res = await fetch(this.getEndpoint(), {
method: "POST",
body: JSON.stringify({
'method': 'torrent-get',
'arguments': { "fields": ["id", "name", "status", "rateDownload", "rateUpload", "totalSize", "percentDone", "files", "peers", "trackers", "trackerStats","hashString"] }
}),
headers: { 'Content-Type': 'application/json' }
});
return res.json();
}
async stopTorrent(ids = []) {
const res = await fetch(this.getEndpoint(), {
method: "POST",
body: JSON.stringify({
'method': 'torrent-stop',
'arguments': ids.length ? { ids } : {},
'tag': 666
}),
headers: { 'Content-Type': 'application/json' }
});
return res.json();
}
async startTorrent(ids = []) {
const res = await fetch(this.getEndpoint(), {
method: "POST",
body: JSON.stringify({
'method': 'torrent-start',
'arguments': ids.length ? { ids } : {},
'tag': 666
}),
headers: { 'Content-Type': 'application/json' }
});
return res.json();
}
}