Spaces:
Paused
Paused
import fs from "fs-extra"; | |
import MemoryStore from "memory-chunk-store"; | |
import os from "os"; | |
import path from "path"; | |
import WebTorrent from "webtorrent"; | |
import { getReadableDuration } from "../utils/file.js"; | |
// Directory to store downloaded files (default OS temp directory) | |
const DOWNLOAD_DIR = process.env.DOWNLOAD_DIR || path.join(os.tmpdir(), "torrent-stream-server"); | |
// Keep downloaded files after all streams are closed (default false) | |
const KEEP_DOWNLOADED_FILES = process.env.KEEP_DOWNLOADED_FILES | |
? process.env.KEEP_DOWNLOADED_FILES === "true" | |
: false; | |
if (!KEEP_DOWNLOADED_FILES) | |
fs.emptyDirSync(DOWNLOAD_DIR); | |
// Maximum number of connections per torrent (default 50) | |
const MAX_CONNS_PER_TORRENT = Number(process.env.MAX_CONNS_PER_TORRENT) || 50; | |
// Max download speed (bytes/s) over all torrents (default 20MB/s) | |
const DOWNLOAD_SPEED_LIMIT = Number(process.env.DOWNLOAD_SPEED_LIMIT) || 20 * 1024 * 1024; | |
// Max upload speed (bytes/s) over all torrents (default 1MB/s) | |
const UPLOAD_SPEED_LIMIT = Number(process.env.UPLOAD_SPEED_LIMIT) || 1 * 1024 * 1024; | |
// Time (ms) to seed torrents after all streams are closed (default 1 minute) | |
const SEED_TIME = Number(process.env.SEED_TIME) || 60 * 1000; | |
// Timeout (ms) when adding torrents if no metadata is received (default 5 seconds) | |
const TORRENT_TIMEOUT = Number(process.env.TORRENT_TIMEOUT) || 5 * 1000; | |
const infoClient = new WebTorrent(); | |
const streamClient = new WebTorrent({ | |
// @ts-ignore | |
downloadLimit: DOWNLOAD_SPEED_LIMIT, | |
uploadLimit: UPLOAD_SPEED_LIMIT, | |
maxConns: MAX_CONNS_PER_TORRENT, | |
}); | |
streamClient.on("torrent", (torrent) => { | |
console.log(`Added torrent: ${torrent.name}`); | |
}); | |
streamClient.on("error", (error) => { | |
if (typeof error === "string") { | |
console.error(`Error: ${error}`); | |
} | |
else { | |
if (error.message.startsWith("Cannot add duplicate torrent")) | |
return; | |
console.error(`Error: ${error.message}`); | |
} | |
}); | |
infoClient.on("error", () => { }); | |
const launchTime = Date.now(); | |
export const getStats = () => ({ | |
uptime: getReadableDuration(Date.now() - launchTime), | |
openStreams: [...openStreams.values()].reduce((a, b) => a + b, 0), | |
downloadSpeed: streamClient.downloadSpeed, | |
uploadSpeed: streamClient.uploadSpeed, | |
activeTorrents: streamClient.torrents.map((torrent) => ({ | |
name: torrent.name, | |
infoHash: torrent.infoHash, | |
size: torrent.length, | |
progress: torrent.progress, | |
downloaded: torrent.downloaded, | |
uploaded: torrent.uploaded, | |
downloadSpeed: torrent.downloadSpeed, | |
uploadSpeed: torrent.uploadSpeed, | |
peers: torrent.numPeers, | |
openStreams: openStreams.get(torrent.infoHash) || 0, | |
files: torrent.files.map((file) => ({ | |
name: file.name, | |
path: file.path, | |
size: file.length, | |
progress: file.progress, | |
downloaded: file.downloaded, | |
})), | |
})), | |
}); | |
export const getOrAddTorrent = (uri) => new Promise((resolve) => { | |
const torrent = streamClient.add(uri, { | |
path: DOWNLOAD_DIR, | |
destroyStoreOnDestroy: !KEEP_DOWNLOADED_FILES, | |
// @ts-ignore | |
deselect: true, | |
}, (torrent) => { | |
clearTimeout(timeout); | |
resolve(torrent); | |
}); | |
const timeout = setTimeout(() => { | |
torrent.destroy(); | |
resolve(undefined); | |
}, TORRENT_TIMEOUT); | |
}); | |
export const getFile = (torrent, path) => torrent.files.find((file) => file.path === path); | |
export const getTorrentInfo = async (uri) => { | |
const getInfo = (torrent) => ({ | |
name: torrent.name, | |
infoHash: torrent.infoHash, | |
size: torrent.length, | |
files: torrent.files.map((file) => ({ | |
name: file.name, | |
path: file.path, | |
size: file.length, | |
})), | |
}); | |
return await new Promise((resolve) => { | |
const torrent = infoClient.add(uri, { store: MemoryStore, destroyStoreOnDestroy: true }, (torrent) => { | |
clearTimeout(timeout); | |
const info = getInfo(torrent); | |
console.log(`Fetched info: ${info.name}`); | |
torrent.destroy(); | |
resolve(info); | |
}); | |
const timeout = setTimeout(() => { | |
torrent.destroy(); | |
resolve(undefined); | |
}, TORRENT_TIMEOUT); | |
}); | |
}; | |
const timeouts = new Map(); | |
const openStreams = new Map(); | |
export const streamOpened = (hash, fileName) => { | |
console.log(`Stream opened: ${fileName}`); | |
const count = openStreams.get(hash) || 0; | |
openStreams.set(hash, count + 1); | |
const timeout = timeouts.get(hash); | |
if (timeout) { | |
clearTimeout(timeout); | |
timeouts.delete(hash); | |
} | |
}; | |
export const streamClosed = (hash, fileName) => { | |
console.log(`Stream closed: ${fileName}`); | |
const count = openStreams.get(hash) || 1; | |
openStreams.set(hash, count - 1); | |
if (count > 1) | |
return; | |
openStreams.delete(hash); | |
let timeout = timeouts.get(hash); | |
if (timeout) | |
return; | |
timeout = setTimeout(async () => { | |
const torrent = await streamClient.get(hash); | |
// @ts-ignore | |
torrent?.destroy(undefined, () => { | |
console.log(`Removed torrent: ${torrent.name}`); | |
timeouts.delete(hash); | |
}); | |
}, SEED_TIME); | |
timeouts.set(hash, timeout); | |
}; | |
//# sourceMappingURL=webtorrent.js.map |