mirror of
https://github.com/PR0M3TH3AN/bitvid-seeder.git
synced 2026-11-04 20:01:11 +00:00
update
This commit is contained in:
1 parent
d87a0e0645
commit
77fecbee1e
4 files changed
+340
-193
No files matched your search
Generated
+127
-1
@@ -8,8 +8,14 @@
|
||||
"name": "simple-torrent-seeder",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"child_process": "^1.0.2",
|
||||
"express": "^4.21.2",
|
||||
"express-rate-limit": "^7.5.0",
|
||||
"fs": "^0.0.1-security",
|
||||
"jsonstream": "^1.0.3",
|
||||
"path": "^0.12.7",
|
||||
"proper-lockfile": "^4.1.2",
|
||||
"url": "^0.11.4",
|
||||
"webtorrent": "^2.5.19",
|
||||
"winston": "^3.17.0"
|
||||
}
|
||||
@@ -694,6 +700,12 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/child_process": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/child_process/-/child_process-1.0.2.tgz",
|
||||
"integrity": "sha512-Wmza/JzL0SiWz7kl6MhIKT5ceIlnFPJX+lwUGj7Clhy5MMldsSoJR0+uvRzOS5Kv45Mq7t1PoE8TsOA9bzvb6g==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/chownr": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
|
||||
@@ -1120,6 +1132,7 @@
|
||||
"version": "4.21.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
|
||||
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
@@ -1165,6 +1178,7 @@
|
||||
"version": "7.5.0",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz",
|
||||
"integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
@@ -1277,6 +1291,12 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/fs": {
|
||||
"version": "0.0.1-security",
|
||||
"resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz",
|
||||
"integrity": "sha512-3XY9e1pP0CVEUCdj5BmfIZxRBTSDycnbqhIOGec9QYtmVH2fbLpj86CFWkrNOkt/Fvty4KZG5lTglL9j/gJ87w==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/fs-chunk-store": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-chunk-store/-/fs-chunk-store-4.1.0.tgz",
|
||||
@@ -1411,6 +1431,12 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
@@ -1592,6 +1618,31 @@
|
||||
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
|
||||
"integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A=="
|
||||
},
|
||||
"node_modules/jsonparse": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.0.0.tgz",
|
||||
"integrity": "sha512-X8/pl4Q98FceotM1YzNYRcdRpYKgKB6nOKW9oNZk4qssFGfSxA6tUyPgEYsZA+MAQ8nR6nBZwE2GKpeeOIeN3A==",
|
||||
"engines": [
|
||||
"node >= 0.2.0"
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jsonstream": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/jsonstream/-/jsonstream-1.0.3.tgz",
|
||||
"integrity": "sha512-yMDWHPGzQRwdiXy6itGgemEviwAoDp5i78y7rdzUa9RVe01vLJku+byVtqxwwSvp1y9LYHWYd+8MkYvbeilCug==",
|
||||
"deprecated": "use JSONStream instead",
|
||||
"dependencies": {
|
||||
"jsonparse": "~1.0.0",
|
||||
"through": ">=2.2.7 <3"
|
||||
},
|
||||
"bin": {
|
||||
"jsonstream": "index.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/junk": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/junk/-/junk-4.0.1.tgz",
|
||||
@@ -2119,6 +2170,16 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/path": {
|
||||
"version": "0.12.7",
|
||||
"resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz",
|
||||
"integrity": "sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"process": "^0.11.1",
|
||||
"util": "^0.10.3"
|
||||
}
|
||||
},
|
||||
"node_modules/path-key": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
@@ -2162,6 +2223,26 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/process": {
|
||||
"version": "0.11.10",
|
||||
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/proper-lockfile": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz",
|
||||
"integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.4",
|
||||
"retry": "^0.12.0",
|
||||
"signal-exit": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
@@ -2183,6 +2264,12 @@
|
||||
"once": "^1.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
|
||||
"integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.13.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||
@@ -2321,6 +2408,15 @@
|
||||
"b4a": "^1.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/retry": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
|
||||
"integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/run-parallel": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||
@@ -2950,6 +3046,19 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/url": {
|
||||
"version": "0.11.4",
|
||||
"resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz",
|
||||
"integrity": "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"punycode": "^1.4.1",
|
||||
"qs": "^6.12.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/ut_metadata": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/ut_metadata/-/ut_metadata-4.0.3.tgz",
|
||||
@@ -3039,11 +3148,26 @@
|
||||
"node": ">=6.14.2"
|
||||
}
|
||||
},
|
||||
"node_modules/util": {
|
||||
"version": "0.10.4",
|
||||
"resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz",
|
||||
"integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
|
||||
},
|
||||
"node_modules/util/node_modules/inherits": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
|
||||
"integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/utils-merge": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||
@@ -3118,6 +3242,7 @@
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@silentbot1/nat-api": "^0.4.7",
|
||||
"@thaunknown/simple-peer": "^10.0.11",
|
||||
@@ -3222,6 +3347,7 @@
|
||||
"version": "3.17.0",
|
||||
"resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz",
|
||||
"integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@colors/colors": "^1.6.0",
|
||||
"@dabh/diagnostics": "^2.0.2",
|
||||
|
||||
+7
-1
@@ -7,8 +7,14 @@
|
||||
"start": "node server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"child_process": "^1.0.2",
|
||||
"express": "^4.21.2",
|
||||
"express-rate-limit": "^7.5.0",
|
||||
"fs": "^0.0.1-security",
|
||||
"jsonstream": "^1.0.3",
|
||||
"path": "^0.12.7",
|
||||
"proper-lockfile": "^4.1.2",
|
||||
"url": "^0.11.4",
|
||||
"webtorrent": "^2.5.19",
|
||||
"winston": "^3.17.0"
|
||||
}
|
||||
|
||||
+60
-15
@@ -157,6 +157,10 @@
|
||||
<!-- Main UI Script -->
|
||||
<script>
|
||||
window.addEventListener("nostrClientReady", () => {
|
||||
// Pagination variables
|
||||
let currentPage = 1;
|
||||
const limit = 10;
|
||||
|
||||
// Profile-related functions
|
||||
async function updateProfileUI(pubkey) {
|
||||
const btnAvatar = document.getElementById("loginButtonAvatar");
|
||||
@@ -281,14 +285,22 @@
|
||||
body: JSON.stringify({ magnet }),
|
||||
});
|
||||
document.getElementById("magnet").value = "";
|
||||
fetchTorrents();
|
||||
fetchTorrents(currentPage);
|
||||
});
|
||||
|
||||
async function fetchTorrents() {
|
||||
async function fetchTorrents(page) {
|
||||
if (!localStorage.getItem("userPubKey")) return;
|
||||
try {
|
||||
const res = await fetch("/torrents");
|
||||
const torrents = await res.json();
|
||||
const res = await fetch(`/torrents?page=${page}&limit=${limit}`);
|
||||
if (!res.ok) {
|
||||
throw new Error(`Server error: ${res.status} ${res.statusText}`);
|
||||
}
|
||||
const data = await res.json();
|
||||
if (!data.torrents) {
|
||||
console.error("No torrents found in response:", data);
|
||||
return;
|
||||
}
|
||||
const { torrents, total, page: current } = data;
|
||||
const list = document.getElementById("torrentList");
|
||||
list.innerHTML = "";
|
||||
torrents.forEach((torrent) => {
|
||||
@@ -321,15 +333,13 @@
|
||||
await fetch("/resume/" + torrent.infoHash, {
|
||||
method: "POST",
|
||||
});
|
||||
fetchTorrents();
|
||||
fetchTorrents(currentPage);
|
||||
};
|
||||
} else {
|
||||
toggleBtn.textContent = "Pause";
|
||||
toggleBtn.onclick = async () => {
|
||||
await fetch("/pause/" + torrent.infoHash, {
|
||||
method: "POST",
|
||||
});
|
||||
fetchTorrents();
|
||||
await fetch("/pause/" + torrent.infoHash, { method: "POST" });
|
||||
fetchTorrents(currentPage);
|
||||
};
|
||||
}
|
||||
tdActions.appendChild(toggleBtn);
|
||||
@@ -340,17 +350,52 @@
|
||||
await fetch("/remove/" + torrent.infoHash, {
|
||||
method: "DELETE",
|
||||
});
|
||||
fetchTorrents();
|
||||
fetchTorrents(currentPage);
|
||||
};
|
||||
tdActions.appendChild(removeBtn);
|
||||
tr.appendChild(tdActions);
|
||||
list.appendChild(tr);
|
||||
});
|
||||
updatePaginationControls(total, current);
|
||||
} catch (error) {
|
||||
console.error("Error fetching torrents:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function updatePaginationControls(total, page) {
|
||||
const totalPages = Math.ceil(total / limit);
|
||||
const pagination = document.createElement("div");
|
||||
pagination.className = "d-flex justify-content-center mt-3";
|
||||
const prevBtn = document.createElement("button");
|
||||
prevBtn.className = "btn btn-secondary me-2";
|
||||
prevBtn.textContent = "Previous";
|
||||
prevBtn.disabled = page === 1;
|
||||
prevBtn.onclick = () => {
|
||||
if (page > 1) {
|
||||
currentPage = page - 1;
|
||||
fetchTorrents(currentPage);
|
||||
}
|
||||
};
|
||||
pagination.appendChild(prevBtn);
|
||||
const nextBtn = document.createElement("button");
|
||||
nextBtn.className = "btn btn-secondary";
|
||||
nextBtn.textContent = "Next";
|
||||
nextBtn.disabled = page === totalPages;
|
||||
nextBtn.onclick = () => {
|
||||
if (page < totalPages) {
|
||||
currentPage = page + 1;
|
||||
fetchTorrents(currentPage);
|
||||
}
|
||||
};
|
||||
pagination.appendChild(nextBtn);
|
||||
const cardBody = document.querySelector(".card-body");
|
||||
const existingPagination = cardBody.querySelector(".d-flex");
|
||||
if (existingPagination) {
|
||||
existingPagination.remove();
|
||||
}
|
||||
cardBody.appendChild(pagination);
|
||||
}
|
||||
|
||||
async function fetchConfig() {
|
||||
if (!localStorage.getItem("userPubKey")) return;
|
||||
try {
|
||||
@@ -382,14 +427,14 @@
|
||||
).hide();
|
||||
});
|
||||
|
||||
// Periodic updates and initial fetch
|
||||
// Initial fetch and periodic updates
|
||||
fetchTorrents(currentPage);
|
||||
fetchConfig();
|
||||
|
||||
setInterval(() => {
|
||||
fetchTorrents();
|
||||
fetchTorrents(currentPage);
|
||||
fetchConfig();
|
||||
}, 5000);
|
||||
|
||||
fetchTorrents();
|
||||
fetchConfig();
|
||||
});
|
||||
</script>
|
||||
<!-- Bootstrap Bundle JS -->
|
||||
|
||||
@@ -11,8 +11,11 @@ import { fileURLToPath } from "url";
|
||||
import rateLimit from "express-rate-limit";
|
||||
import winston from "winston";
|
||||
import { exec } from "child_process";
|
||||
import JSONStream from "JSONStream";
|
||||
import { lock, unlock } from "proper-lockfile";
|
||||
import os from "os"; // Added for CPU and memory monitoring
|
||||
|
||||
// Set up Winston logger (debug level)
|
||||
// **Logger Setup**
|
||||
const logger = winston.createLogger({
|
||||
level: "debug",
|
||||
format: winston.format.combine(
|
||||
@@ -24,16 +27,16 @@ const logger = winston.createLogger({
|
||||
transports: [new winston.transports.Console()],
|
||||
});
|
||||
|
||||
// **App and Client Initialization**
|
||||
const app = express();
|
||||
const port = process.env.PORT || 3000;
|
||||
const client = new WebTorrent();
|
||||
const client = new WebTorrent({ maxWebConns: 200 });
|
||||
client.setMaxListeners(50);
|
||||
|
||||
// ES module __dirname workaround
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
// Rate limiting middleware
|
||||
// **Rate Limiting Middleware**
|
||||
const limiter = rateLimit({
|
||||
windowMs: 60 * 1000,
|
||||
max: 100,
|
||||
@@ -41,17 +44,14 @@ const limiter = rateLimit({
|
||||
app.use(limiter);
|
||||
app.use(express.json());
|
||||
|
||||
// --- Global Variables and Helper Functions ---
|
||||
|
||||
// Configuration
|
||||
// **Configuration and Global Variables**
|
||||
const CONFIG_FILE = path.join(__dirname, "config.json");
|
||||
let config = { storageDir: path.join(__dirname, "downloads") };
|
||||
|
||||
// Torrents data
|
||||
const TORRENTS_FILE = path.join(__dirname, "torrents.json");
|
||||
let savedTorrents = [];
|
||||
|
||||
// Helper: Check if a path exists
|
||||
// **Helper Functions**
|
||||
async function pathExists(p) {
|
||||
try {
|
||||
await fsPromises.access(p);
|
||||
@@ -61,13 +61,21 @@ async function pathExists(p) {
|
||||
}
|
||||
}
|
||||
|
||||
// Load configuration from file
|
||||
function formatBytes(bytes) {
|
||||
if (bytes === 0) return "0 B";
|
||||
const k = 1024;
|
||||
const sizes = ["B", "KB", "MB", "GB", "TB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return (bytes / Math.pow(k, i)).toFixed(2) + " " + sizes[i];
|
||||
}
|
||||
|
||||
// **Load Configuration**
|
||||
async function loadConfig() {
|
||||
try {
|
||||
if (await pathExists(CONFIG_FILE)) {
|
||||
const data = await fsPromises.readFile(CONFIG_FILE, "utf-8");
|
||||
config = JSON.parse(data);
|
||||
config.storageDir = path.resolve(__dirname, config.storageDir);
|
||||
config.storageDir = path.resolve(config.storageDir); // Ensure absolute path
|
||||
} else {
|
||||
await fsPromises.writeFile(
|
||||
CONFIG_FILE,
|
||||
@@ -80,41 +88,61 @@ async function loadConfig() {
|
||||
}
|
||||
await loadConfig();
|
||||
|
||||
// Load saved torrents from file
|
||||
// **Load Torrents with Streaming**
|
||||
async function loadSavedTorrentsFromFile() {
|
||||
try {
|
||||
if (fs.existsSync(TORRENTS_FILE)) {
|
||||
const data = await fsPromises.readFile(TORRENTS_FILE, "utf-8");
|
||||
savedTorrents = JSON.parse(data);
|
||||
logger.info(`Loaded saved torrents: ${JSON.stringify(savedTorrents)}`);
|
||||
} else {
|
||||
logger.info("No torrents.json file found. Starting with an empty list.");
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error("Error loading torrents file: " + err);
|
||||
if (!fs.existsSync(TORRENTS_FILE)) {
|
||||
logger.info("No torrents.json file found. Starting with an empty list.");
|
||||
return;
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
const stream = fs.createReadStream(TORRENTS_FILE, { encoding: "utf8" });
|
||||
const parser = JSONStream.parse("*");
|
||||
savedTorrents = [];
|
||||
parser.on("data", (torrent) => savedTorrents.push(torrent));
|
||||
parser.on("end", () => resolve());
|
||||
parser.on("error", (err) => reject(err));
|
||||
stream.pipe(parser);
|
||||
});
|
||||
}
|
||||
await loadSavedTorrentsFromFile();
|
||||
|
||||
// Save torrents to file
|
||||
// **Batch Save Torrents with File Locking**
|
||||
let pendingChanges = false;
|
||||
|
||||
async function saveTorrents() {
|
||||
try {
|
||||
await fsPromises.writeFile(
|
||||
TORRENTS_FILE,
|
||||
JSON.stringify(savedTorrents, null, 2)
|
||||
);
|
||||
} catch (err) {
|
||||
logger.error("Error saving torrents file: " + err);
|
||||
}
|
||||
pendingChanges = true;
|
||||
}
|
||||
|
||||
// Ensure storage directory exists
|
||||
setInterval(async () => {
|
||||
if (pendingChanges) {
|
||||
await lock(TORRENTS_FILE);
|
||||
try {
|
||||
const stream = JSONStream.stringify("[\n", ",\n", "\n]");
|
||||
const fileStream = fs.createWriteStream(TORRENTS_FILE);
|
||||
stream.pipe(fileStream);
|
||||
savedTorrents.forEach((torrent) => stream.write(torrent));
|
||||
stream.end();
|
||||
await new Promise((resolve, reject) => {
|
||||
fileStream.on("finish", resolve);
|
||||
fileStream.on("error", reject);
|
||||
});
|
||||
pendingChanges = false;
|
||||
logger.info("Torrents saved to file");
|
||||
} catch (err) {
|
||||
logger.error("Error saving torrents: " + err);
|
||||
} finally {
|
||||
await unlock(TORRENTS_FILE);
|
||||
}
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
// **Ensure Storage Directory Exists**
|
||||
let STORAGE_DIR = config.storageDir;
|
||||
if (!fs.existsSync(STORAGE_DIR)) {
|
||||
fs.mkdirSync(STORAGE_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
// Attach torrent debug events
|
||||
// **Torrent Event Debugging**
|
||||
function attachTorrentDebugEvents(torrent) {
|
||||
torrent.on("wire", (wire, addr) => {
|
||||
logger.debug(`Torrent ${torrent.infoHash} connected to peer: ${addr}`);
|
||||
@@ -127,8 +155,7 @@ function attachTorrentDebugEvents(torrent) {
|
||||
});
|
||||
}
|
||||
|
||||
// --- Resume Active Torrents on Startup ---
|
||||
// For each saved torrent that is not paused, add (or seed) it.
|
||||
// **Resume Active Torrents**
|
||||
async function resumeActiveTorrents() {
|
||||
logger.info("Resuming active torrents...");
|
||||
for (const torrent of savedTorrents) {
|
||||
@@ -139,7 +166,7 @@ async function resumeActiveTorrents() {
|
||||
if (exists) {
|
||||
client.seed(
|
||||
torrentPath,
|
||||
{ name: torrent.name, keepSeeding: true },
|
||||
{ name: torrent.name },
|
||||
(torrentInstance) => {
|
||||
attachTorrentDebugEvents(torrentInstance);
|
||||
logger.info(
|
||||
@@ -150,7 +177,7 @@ async function resumeActiveTorrents() {
|
||||
} else {
|
||||
client.add(
|
||||
torrent.magnet,
|
||||
{ path: STORAGE_DIR, keepSeeding: true },
|
||||
{ path: STORAGE_DIR },
|
||||
(torrentInstance) => {
|
||||
attachTorrentDebugEvents(torrentInstance);
|
||||
logger.info(
|
||||
@@ -167,8 +194,9 @@ async function resumeActiveTorrents() {
|
||||
}
|
||||
await resumeActiveTorrents();
|
||||
|
||||
// --- API Routes ---
|
||||
// GET /config - Return current configuration as JSON
|
||||
// **API Routes**
|
||||
|
||||
// Get configuration
|
||||
app.get("/config", async (req, res) => {
|
||||
try {
|
||||
res.json({ storageDir: config.storageDir });
|
||||
@@ -178,9 +206,14 @@ app.get("/config", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// GET /torrents - Return both active and paused torrents
|
||||
// List torrents with pagination
|
||||
app.get("/torrents", (req, res) => {
|
||||
try {
|
||||
const page = parseInt(req.query.page) || 1;
|
||||
const limit = parseInt(req.query.limit) || 10;
|
||||
const start = (page - 1) * limit;
|
||||
const end = start + limit;
|
||||
|
||||
const active = client.torrents.map((t) => ({
|
||||
infoHash: t.infoHash,
|
||||
name: t.name,
|
||||
@@ -201,16 +234,29 @@ app.get("/torrents", (req, res) => {
|
||||
uploaded: 0,
|
||||
paused: true,
|
||||
}));
|
||||
const torrents = [...active, ...paused];
|
||||
logger.info("GET /torrents returning: " + JSON.stringify(torrents));
|
||||
res.json(torrents);
|
||||
const allTorrents = [...active, ...paused];
|
||||
const paginatedTorrents = allTorrents.slice(start, end);
|
||||
|
||||
res.json({
|
||||
torrents: paginatedTorrents,
|
||||
total: allTorrents.length,
|
||||
page,
|
||||
limit,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error("Error listing torrents: " + err);
|
||||
res.status(500).json({ error: "Error listing torrents" });
|
||||
logger.error("Error listing torrents: " + err.message);
|
||||
res.status(500).json({
|
||||
torrents: [],
|
||||
total: 0,
|
||||
page: parseInt(req.query.page) || 1,
|
||||
limit: parseInt(req.query.limit) || 10,
|
||||
error: "Error listing torrents",
|
||||
message: err.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// POST /pause/:infoHash - Pause a torrent
|
||||
// Pause a torrent
|
||||
app.post("/pause/:infoHash", async (req, res) => {
|
||||
const infoHash = req.params.infoHash;
|
||||
const torrent = client.get(infoHash);
|
||||
@@ -226,13 +272,12 @@ app.post("/pause/:infoHash", async (req, res) => {
|
||||
}
|
||||
const entry = savedTorrents.find((s) => s.infoHash === infoHash);
|
||||
if (entry) entry.paused = true;
|
||||
await saveTorrents();
|
||||
logger.info(`Torrent paused: ${infoHash}`);
|
||||
saveTorrents();
|
||||
res.json({ message: "Torrent paused" });
|
||||
});
|
||||
});
|
||||
|
||||
// POST /resume/:infoHash - Resume a paused torrent
|
||||
// Resume a torrent
|
||||
app.post("/resume/:infoHash", async (req, res) => {
|
||||
const infoHash = req.params.infoHash;
|
||||
const entry = savedTorrents.find((s) => s.infoHash === infoHash);
|
||||
@@ -240,32 +285,22 @@ app.post("/resume/:infoHash", async (req, res) => {
|
||||
return res.status(400).json({ error: "Torrent not found in saved data" });
|
||||
}
|
||||
entry.paused = false;
|
||||
await saveTorrents();
|
||||
saveTorrents();
|
||||
const torrentPath = path.join(STORAGE_DIR, entry.name);
|
||||
const exists = await pathExists(torrentPath);
|
||||
try {
|
||||
if (exists) {
|
||||
client.seed(
|
||||
torrentPath,
|
||||
{ name: entry.name, keepSeeding: true },
|
||||
(torrent) => {
|
||||
attachTorrentDebugEvents(torrent);
|
||||
logger.info(
|
||||
`Torrent resumed (seeding from file): ${torrent.infoHash}`
|
||||
);
|
||||
res.json({ message: "Torrent resumed" });
|
||||
}
|
||||
);
|
||||
client.seed(torrentPath, { name: entry.name }, (torrent) => {
|
||||
attachTorrentDebugEvents(torrent);
|
||||
logger.info(`Torrent resumed (seeding from file): ${torrent.infoHash}`);
|
||||
res.json({ message: "Torrent resumed" });
|
||||
});
|
||||
} else {
|
||||
client.add(
|
||||
entry.magnet,
|
||||
{ path: STORAGE_DIR, keepSeeding: true },
|
||||
(torrent) => {
|
||||
attachTorrentDebugEvents(torrent);
|
||||
logger.info(`Torrent resumed (downloading): ${torrent.infoHash}`);
|
||||
res.json({ message: "Torrent resumed" });
|
||||
}
|
||||
);
|
||||
client.add(torrent.magnet, { path: STORAGE_DIR }, (torrent) => {
|
||||
attachTorrentDebugEvents(torrent);
|
||||
logger.info(`Torrent resumed (downloading): ${torrent.infoHash}`);
|
||||
res.json({ message: "Torrent resumed" });
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error("Error resuming torrent: " + err);
|
||||
@@ -273,30 +308,34 @@ app.post("/resume/:infoHash", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// POST /config - Update storage directory
|
||||
// Update configuration with path validation
|
||||
app.post("/config", async (req, res) => {
|
||||
const { storageDir } = req.body;
|
||||
if (!storageDir || typeof storageDir !== "string") {
|
||||
return res.status(400).json({ error: "Invalid storageDir provided" });
|
||||
}
|
||||
try {
|
||||
STORAGE_DIR = path.resolve(__dirname, storageDir);
|
||||
config.storageDir = STORAGE_DIR;
|
||||
if (!fs.existsSync(STORAGE_DIR)) {
|
||||
fs.mkdirSync(STORAGE_DIR, { recursive: true });
|
||||
const resolvedPath = path.resolve(storageDir);
|
||||
if (!path.isAbsolute(resolvedPath)) {
|
||||
throw new Error("Path must be absolute");
|
||||
}
|
||||
if (!fs.existsSync(resolvedPath)) {
|
||||
fs.mkdirSync(resolvedPath, { recursive: true });
|
||||
}
|
||||
STORAGE_DIR = resolvedPath;
|
||||
config.storageDir = resolvedPath;
|
||||
await fsPromises.writeFile(
|
||||
CONFIG_FILE,
|
||||
JSON.stringify({ storageDir }, null, 2)
|
||||
JSON.stringify({ storageDir: resolvedPath }, null, 2)
|
||||
);
|
||||
res.json({ message: "Config updated", storageDir: STORAGE_DIR });
|
||||
res.json({ message: "Config updated", storageDir: resolvedPath });
|
||||
} catch (err) {
|
||||
logger.error("Error updating config: " + err);
|
||||
logger.error("Error updating config: " + err.message);
|
||||
res.status(500).json({ error: "Error updating config" });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /add - Add a new torrent
|
||||
// Add a new torrent
|
||||
app.post("/add", (req, res) => {
|
||||
const { magnet } = req.body;
|
||||
if (!magnet || typeof magnet !== "string") {
|
||||
@@ -310,7 +349,7 @@ app.post("/add", (req, res) => {
|
||||
return res.status(409).json({ error: "Torrent already added." });
|
||||
}
|
||||
try {
|
||||
client.add(magnet, { path: STORAGE_DIR, keepSeeding: true }, (torrent) => {
|
||||
client.add(magnet, { path: STORAGE_DIR }, (torrent) => {
|
||||
attachTorrentDebugEvents(torrent);
|
||||
logger.info(`Torrent added: ${torrent.infoHash}`);
|
||||
const torrentData = {
|
||||
@@ -329,7 +368,7 @@ app.post("/add", (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE /remove/:infoHash - Remove a torrent completely.
|
||||
// Remove a torrent
|
||||
app.delete("/remove/:infoHash", (req, res) => {
|
||||
const infoHash = req.params.infoHash;
|
||||
if (!infoHash || typeof infoHash !== "string") {
|
||||
@@ -349,7 +388,7 @@ app.delete("/remove/:infoHash", (req, res) => {
|
||||
return res.status(500).json({ error: "Error removing torrent" });
|
||||
}
|
||||
savedTorrents = savedTorrents.filter((t) => t.infoHash !== infoHash);
|
||||
await saveTorrents();
|
||||
saveTorrents();
|
||||
const torrentPath = path.join(STORAGE_DIR, torrentName);
|
||||
if (await pathExists(torrentPath)) {
|
||||
try {
|
||||
@@ -367,107 +406,38 @@ app.delete("/remove/:infoHash", (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// --- End API Routes ---
|
||||
|
||||
// Serve static files from "public"
|
||||
// **Serve Static Files**
|
||||
app.use(express.static("public"));
|
||||
|
||||
// Log system limits periodically
|
||||
function logSystemLimits() {
|
||||
const platform = process.platform;
|
||||
if (platform === "win32") {
|
||||
logger.info("Running on Windows.");
|
||||
const activeHandles = process._getActiveHandles
|
||||
? process._getActiveHandles().length
|
||||
: "N/A";
|
||||
logger.info(`Active handles: ${activeHandles}`);
|
||||
exec("netsh int tcp show global", (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
logger.error(`Error running netsh: ${err.message}`);
|
||||
} else {
|
||||
logger.info(`TCP Global Settings:\n${stdout}`);
|
||||
}
|
||||
});
|
||||
} else if (platform === "linux") {
|
||||
exec('bash -c "ulimit -n"', (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
logger.error(`Error fetching file descriptor limit: ${err.message}`);
|
||||
} else {
|
||||
logger.info(`File descriptor limit: ${stdout.trim()}`);
|
||||
}
|
||||
});
|
||||
exec(`lsof -p ${process.pid} | wc -l`, (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
logger.error(`Error counting open file descriptors: ${err.message}`);
|
||||
} else {
|
||||
logger.info(`Current open file descriptors: ${stdout.trim()}`);
|
||||
}
|
||||
});
|
||||
exec("cat /proc/sys/net/core/rmem_max", (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
logger.error(`Error fetching rmem_max: ${err.message}`);
|
||||
} else {
|
||||
logger.info(`Receive buffer max (rmem_max): ${stdout.trim()}`);
|
||||
}
|
||||
});
|
||||
exec("cat /proc/sys/net/core/wmem_max", (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
logger.error(`Error fetching wmem_max: ${err.message}`);
|
||||
} else {
|
||||
logger.info(`Send buffer max (wmem_max): ${stdout.trim()}`);
|
||||
}
|
||||
});
|
||||
} else if (platform === "darwin") {
|
||||
exec("ulimit -n", (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
logger.error(`Error fetching file descriptor limit: ${err.message}`);
|
||||
} else {
|
||||
logger.info(`File descriptor limit: ${stdout.trim()}`);
|
||||
}
|
||||
});
|
||||
exec(`lsof -p ${process.pid} | wc -l`, (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
logger.error(`Error counting open file descriptors: ${err.message}`);
|
||||
} else {
|
||||
logger.info(`Current open file descriptors: ${stdout.trim()}`);
|
||||
}
|
||||
});
|
||||
exec("sysctl kern.ipc.maxsockbuf", (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
logger.error(`Error fetching kern.ipc.maxsockbuf: ${err.message}`);
|
||||
} else {
|
||||
logger.info(`kern.ipc.maxsockbuf: ${stdout.trim()}`);
|
||||
}
|
||||
});
|
||||
exec("sysctl net.inet.tcp.recvspace", (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
logger.error(`Error fetching net.inet.tcp.recvspace: ${err.message}`);
|
||||
} else {
|
||||
logger.info(`net.inet.tcp.recvspace: ${stdout.trim()}`);
|
||||
}
|
||||
});
|
||||
exec("sysctl net.inet.tcp.sendspace", (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
logger.error(`Error fetching net.inet.tcp.sendspace: ${err.message}`);
|
||||
} else {
|
||||
logger.info(`net.inet.tcp.sendspace: ${stdout.trim()}`);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
logger.info(
|
||||
`Platform ${platform} not specifically supported for system limits logging.`
|
||||
);
|
||||
}
|
||||
}
|
||||
logSystemLimits();
|
||||
setInterval(logSystemLimits, 60000);
|
||||
// **Resource Usage Monitoring**
|
||||
setInterval(() => {
|
||||
const cpuUsage = os.loadavg()[0]; // 1-minute load average
|
||||
const freeMem = os.freemem();
|
||||
const totalMem = os.totalmem();
|
||||
const memUsage = ((totalMem - freeMem) / totalMem) * 100;
|
||||
logger.info(
|
||||
`CPU Load: ${cpuUsage.toFixed(2)}%, Memory Usage: ${memUsage.toFixed(2)}%`
|
||||
);
|
||||
|
||||
// Start the server
|
||||
const mem = process.memoryUsage();
|
||||
logger.info(
|
||||
`Memory: RSS=${formatBytes(mem.rss)}, Heap=${formatBytes(
|
||||
mem.heapUsed
|
||||
)}/${formatBytes(mem.heapTotal)}`
|
||||
);
|
||||
logger.info(
|
||||
`Active torrents: ${client.torrents.length}, Paused torrents: ${
|
||||
savedTorrents.filter((t) => t.paused).length
|
||||
}`
|
||||
);
|
||||
}, 60000); // Log every minute
|
||||
|
||||
// **Start Server**
|
||||
const server = app.listen(port, () => {
|
||||
logger.info(`Server running on http://localhost:${port}`);
|
||||
});
|
||||
|
||||
// Graceful shutdown
|
||||
// **Graceful Shutdown**
|
||||
function shutdown() {
|
||||
logger.info("Shutting down server...");
|
||||
server.close(() => {
|
||||
|
||||
Reference in new issue
Block a user