mirror of
https://github.com/PR0M3TH3AN/bitvid-seeder.git
synced 2026-10-27 07:51:10 +00:00
268 lines
8.5 KiB
JavaScript
268 lines
8.5 KiB
JavaScript
import { EventEmitter } from 'events';
|
|
EventEmitter.defaultMaxListeners = 50;
|
|
|
|
import express from 'express';
|
|
import WebTorrent from 'webtorrent';
|
|
import fs from 'fs';
|
|
import { promises as fsPromises } from 'fs';
|
|
import path from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
import rateLimit from 'express-rate-limit';
|
|
import winston from 'winston';
|
|
|
|
// Set up Winston logger
|
|
const logger = winston.createLogger({
|
|
level: 'info',
|
|
format: winston.format.combine(
|
|
winston.format.timestamp(),
|
|
winston.format.printf(
|
|
({ timestamp, level, message }) => `${timestamp} ${level}: ${message}`
|
|
)
|
|
),
|
|
transports: [new winston.transports.Console()]
|
|
});
|
|
|
|
const app = express();
|
|
const port = process.env.PORT || 3000;
|
|
const client = new WebTorrent();
|
|
|
|
// Increase max listeners for the client to avoid warnings (if needed)
|
|
client.setMaxListeners(50);
|
|
|
|
// ES module __dirname workaround
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
|
|
// Rate limiting middleware (100 requests per minute per IP)
|
|
const limiter = rateLimit({
|
|
windowMs: 60 * 1000,
|
|
max: 100
|
|
});
|
|
app.use(limiter);
|
|
|
|
// Serve static files and parse JSON bodies
|
|
app.use(express.static('public'));
|
|
app.use(express.json());
|
|
|
|
// Load configuration from config.json
|
|
const CONFIG_FILE = path.join(__dirname, 'config.json');
|
|
let config = { storageDir: path.join(__dirname, 'downloads') };
|
|
|
|
try {
|
|
if (fs.existsSync(CONFIG_FILE)) {
|
|
const configData = fs.readFileSync(CONFIG_FILE, 'utf-8');
|
|
config = JSON.parse(configData);
|
|
// Resolve storageDir relative to __dirname
|
|
config.storageDir = path.resolve(__dirname, config.storageDir);
|
|
} else {
|
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify({ storageDir: './downloads' }, null, 2));
|
|
}
|
|
} catch (err) {
|
|
logger.error('Error reading config file, using default config: ' + err);
|
|
}
|
|
|
|
let STORAGE_DIR = config.storageDir;
|
|
if (!fs.existsSync(STORAGE_DIR)) {
|
|
fs.mkdirSync(STORAGE_DIR, { recursive: true });
|
|
}
|
|
|
|
// JSON file to store torrent metadata
|
|
const TORRENTS_FILE = path.join(__dirname, 'torrents.json');
|
|
let savedTorrents = [];
|
|
try {
|
|
if (fs.existsSync(TORRENTS_FILE)) {
|
|
const data = fs.readFileSync(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);
|
|
}
|
|
|
|
function saveTorrents() {
|
|
try {
|
|
fs.writeFileSync(TORRENTS_FILE, JSON.stringify(savedTorrents, null, 2));
|
|
} catch (err) {
|
|
logger.error('Error saving torrents file: ' + err);
|
|
}
|
|
}
|
|
|
|
// Helper: Check if a path exists asynchronously
|
|
async function pathExists(p) {
|
|
try {
|
|
await fsPromises.access(p);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Load saved torrents concurrently on startup.
|
|
// For each saved torrent, if the expected file/folder exists, we seed from that location;
|
|
// otherwise, we add the torrent to download.
|
|
async function loadSavedTorrents() {
|
|
await Promise.all(
|
|
savedTorrents.map(async torrentData => {
|
|
const torrentPath = path.join(STORAGE_DIR, torrentData.name);
|
|
const exists = await pathExists(torrentPath);
|
|
try {
|
|
if (exists) {
|
|
// If the file exists, seed it to keep it active.
|
|
client.seed(torrentPath, { name: torrentData.name, keepSeeding: true }, torrent => {
|
|
logger.info(`Seeding torrent from existing files: ${torrent.infoHash}`);
|
|
});
|
|
} else {
|
|
// Otherwise, add the torrent to download its files.
|
|
client.add(torrentData.magnet, { path: STORAGE_DIR, keepSeeding: true }, torrent => {
|
|
logger.info(`Downloading torrent: ${torrent.infoHash}`);
|
|
});
|
|
}
|
|
} catch (err) {
|
|
logger.error('Error loading saved torrent on startup: ' + err);
|
|
}
|
|
})
|
|
);
|
|
}
|
|
loadSavedTorrents();
|
|
|
|
// GET /config - Return current storage configuration
|
|
app.get('/config', (req, res) => {
|
|
res.json({ storageDir: STORAGE_DIR });
|
|
});
|
|
|
|
// POST /config - Update storage directory (with input validation)
|
|
app.post('/config', (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 });
|
|
}
|
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify({ storageDir }, null, 2));
|
|
res.json({ message: 'Config updated', storageDir: STORAGE_DIR });
|
|
} catch (err) {
|
|
logger.error('Error updating config: ' + err);
|
|
res.status(500).json({ error: 'Error updating config' });
|
|
}
|
|
});
|
|
|
|
// POST /add - Add a new torrent (with basic validation and duplicate check)
|
|
// The keepSeeding option ensures that the torrent remains active.
|
|
app.post('/add', (req, res) => {
|
|
const { magnet } = req.body;
|
|
if (!magnet || typeof magnet !== 'string') {
|
|
return res.status(400).json({ error: 'No valid magnet link provided' });
|
|
}
|
|
if (!magnet.startsWith('magnet:?')) {
|
|
return res.status(400).json({ error: 'Invalid magnet link format' });
|
|
}
|
|
// Check if the torrent already exists in saved metadata.
|
|
if (savedTorrents.some(t => t.magnet === magnet)) {
|
|
logger.info('Attempted to add duplicate torrent.');
|
|
return res.status(409).json({ error: 'Torrent already added.' });
|
|
}
|
|
try {
|
|
client.add(magnet, { path: STORAGE_DIR, keepSeeding: true }, torrent => {
|
|
logger.info(`Torrent added: ${torrent.infoHash}`);
|
|
const torrentData = {
|
|
magnet,
|
|
infoHash: torrent.infoHash,
|
|
name: torrent.name
|
|
};
|
|
savedTorrents.push(torrentData);
|
|
saveTorrents();
|
|
res.json(torrentData);
|
|
});
|
|
} catch (err) {
|
|
logger.error('Error adding torrent: ' + err);
|
|
res.status(500).json({ error: 'Error adding torrent' });
|
|
}
|
|
});
|
|
|
|
// GET /torrents - List active torrents with extra info
|
|
app.get('/torrents', (req, res) => {
|
|
try {
|
|
const torrents = client.torrents.map(t => ({
|
|
infoHash: t.infoHash,
|
|
name: t.name,
|
|
progress: t.progress,
|
|
numPeers: t.numPeers,
|
|
downloaded: t.downloaded, // in bytes
|
|
uploaded: t.uploaded // in bytes
|
|
}));
|
|
logger.info('GET /torrents returning: ' + JSON.stringify(torrents));
|
|
res.json(torrents);
|
|
} catch (err) {
|
|
logger.error('Error listing torrents: ' + err);
|
|
res.status(500).json({ error: 'Error listing torrents' });
|
|
}
|
|
});
|
|
|
|
// DELETE /remove/:infoHash - Remove a torrent
|
|
app.delete('/remove/:infoHash', (req, res) => {
|
|
const infoHash = req.params.infoHash;
|
|
if (!infoHash || typeof infoHash !== 'string') {
|
|
return res.status(400).json({ error: 'Invalid infoHash' });
|
|
}
|
|
const torrent = client.get(infoHash);
|
|
const savedData = savedTorrents.find(t => t.infoHash === infoHash);
|
|
// Use the torrent's name if available; otherwise, use saved metadata.
|
|
const torrentName = (torrent && torrent.name) || (savedData && savedData.name);
|
|
if (!torrentName) {
|
|
return res.status(400).json({ error: 'Torrent name not found' });
|
|
}
|
|
try {
|
|
client.remove(infoHash, { destroyStore: true }, async err => {
|
|
if (err) {
|
|
logger.error('Error removing torrent: ' + err);
|
|
return res.status(500).json({ error: 'Error removing torrent' });
|
|
}
|
|
savedTorrents = savedTorrents.filter(t => t.infoHash !== infoHash);
|
|
saveTorrents();
|
|
const torrentPath = path.join(STORAGE_DIR, torrentName);
|
|
if (await pathExists(torrentPath)) {
|
|
try {
|
|
await fsPromises.rm(torrentPath, { recursive: true, force: true });
|
|
} catch (rmErr) {
|
|
logger.error('Error removing torrent files: ' + rmErr);
|
|
}
|
|
}
|
|
logger.info(`Torrent removed: ${infoHash}`);
|
|
res.json({ message: 'Torrent removed' });
|
|
});
|
|
} catch (err) {
|
|
logger.error('Error processing removal: ' + err);
|
|
res.status(500).json({ error: 'Error processing removal' });
|
|
}
|
|
});
|
|
|
|
// Start the server
|
|
const server = app.listen(port, () => {
|
|
logger.info(`Server running on http://localhost:${port}`);
|
|
});
|
|
|
|
// Graceful shutdown
|
|
function shutdown() {
|
|
logger.info('Shutting down server...');
|
|
server.close(() => {
|
|
logger.info('HTTP server closed.');
|
|
client.destroy(err => {
|
|
if (err) {
|
|
logger.error('Error destroying WebTorrent client: ' + err);
|
|
} else {
|
|
logger.info('WebTorrent client destroyed.');
|
|
}
|
|
process.exit(0);
|
|
});
|
|
});
|
|
}
|
|
|
|
process.on('SIGINT', shutdown);
|
|
process.on('SIGTERM', shutdown);
|