mirror of
https://github.com/PurpleI2P/i2pd.git
synced 2026-10-23 02:11:43 +00:00
invoke torrents web service from webconsole
This commit is contained in:
1 parent
35db1e0bd2
commit
8248c75ae1
4 files changed
+447
-4
No files matched your search
@@ -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>
|
||||
|
||||
@@ -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
@@ -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> ⇐ ";
|
||||
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";
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user