invoke torrents web service from webconsole

This commit is contained in:
orignal committed 2026-10-12 15:02:57 -04:00
1 parent 35db1e0bd2
commit 8248c75ae1
4 files changed
+447 -4

No files matched your search

+314
View File
@@ -0,0 +1,314 @@
<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;
}
#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;
}
</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>
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");
}
</script>
+91
View File
@@ -0,0 +1,91 @@
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();
}
}
+39 -3
View File
@@ -1210,20 +1210,56 @@ namespace http {
s << "</div>\r\n<br>\r\n";
}
#ifndef NO_TORRENTS
auto findTunnelEndpoint = [](const std::string& tunnelname) -> std::pair<uint16_t, std::string>
{
LogPrint(eLogDebug,"HTTPServer: GetTorrentsRPC");
for (auto& [port, server] : i2p::client::context.GetTorrentsRPCServers())
{
if (!server)
{
LogPrint(eLogError,"HTTPServer: GetTorrentsRPCServers return nothing");
continue;
}
for (const auto& [rpcPath, weakTunnel] : server->GetTunnels())
{
if (auto tunnel = weakTunnel.lock())
{
LogPrint(eLogDebug, "HTTPServer: TUNNEL_OPENJS", tunnel->GetName());
if (tunnel->GetName() == tunnelname)
{
uint16_t rpcPort = server->GetAcceptor().local_endpoint().port();
return { rpcPort, rpcPath };
}
}
}
}
return { 0, "" };
};
auto& torrentsTunnels = i2p::client::context.GetTorrentsTunnels ();
if (!torrentsTunnels.empty ())
{
s << "<br>\r\n<b>" << tr("Torrents") << ":</b><br>\r\n<div class=\"list\">\r\n";
for (auto& it: torrentsTunnels)
{
auto& ident = it.second->GetLocalDestination ()->GetIdentHash();
s << "<div class=\"listitem\"><a href=\"" << webroot << "?page=" << HTTP_PAGE_LOCAL_DESTINATION << "&b32=" << ident.ToBase32 () << "\">";
s << it.second->GetName () << "</a> &#8656; ";
s << i2p::client::context.GetAddressBook ().ToAddress(ident);
s << "<a href='?page=" << OPENJS_TORRENT_CLIENT_PAGE << "&tunnelname=" << it.second-> GetName()<< "'> | Open Torrent Client for this Tunnel</a>";
s << "</div>\r\n"<< std::endl;
auto [rpcPort, rpcPath] = findTunnelEndpoint(it.second-> GetName());
if (rpcPort && rpcPath.length() && it.second-> SupportsWeb()) {
auto webPath = rpcPath;
webPath.erase(webPath.size() - 4); // removing "/rpc" suffix
std::string jsRedir = "this.href = location.protocol + '//' + location.hostname + ':' + " + std::to_string(rpcPort) + " + '" + webPath + "/web/'";
s << "<a href='http://localhost:" << rpcPort << webPath << "/web/' "
<< "onmouseover=\"" << jsRedir << "\" "
<< "onclick=\"" << jsRedir << "\">"
<< " | Web Panel</a>";
}
s << "</div>\r\n"<< std::endl;
}
s << "</div>\r\n<br>\r\n";
}
+3 -1
View File
@@ -946,13 +946,15 @@ namespace torrents
subUrlPath++;
if (subUrlPath != urlPath.end ())
{
auto tunnel = m_Server.GetTunnel (subUrlPath->string () + "/rpc");
auto tunnel = m_Server.GetTunnel (std::string ("/") + subUrlPath->string () + "/rpc");
if (tunnel && tunnel->SupportsWeb ())
{
subUrlPath++;
if (subUrlPath != urlPath.end ())
isPathValid = *subUrlPath == "web";
}
else
LogPrint (eLogDebug, "TorrentsRPC: Tunnel not found or web is not supported ", subUrlPath->string ());
}
}
if (!isPathValid)