3 Commits

Author SHA1 Message Date
72bd880950 added SpigotMC!
Well its technically its Spiget, but same diff
2025-12-14 00:05:09 -08:00
aee24d9492 The Big Three! (V1.1.1)
Added concurrency control for downloads using p-limit (max 6 simultaneous).

Removed cli-progress and progress bars from downloads, as it shits the bed with concurrency.

Simplified axios requests in downloadJar (removed custom headers as it was used only for cli-progress).

Refactored Jenkins handling to use API JSON instead of HTML scraping. (as i had weird failing with a few jenkins servers, and this seemed to fix it completely)

Removed cli-progress dependency; added p-limit dependency.
2025-12-13 20:04:37 -08:00
da7a8c5f3e Updated to 1.1.0 and changes :P
- Fixed PaperMC hanger
- added PaperMC server downloader
- made it so that you set your remote path as the root of your minecraft server, and the script figures out where the files need to go
- Fixed Modrinth so that it also handles server distros like how Jenkins handler does it
- Added minecraft distro and version to confing.json.example, this is used right now, but i will be wrighting it into the script soon
- added LINCENSE file
2025-07-23 12:42:26 -07:00
5 changed files with 208 additions and 83 deletions

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Sophia Atkinson
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -5,7 +5,8 @@
## Features
- Auto-detects download source (GitHub / Jenkins / Modrinth / Bukkit / Direct)
- Downloads the Latest PaperMC server version (may add more soon)
- Auto-detects download source (GitHub / Jenkins / Modrinth / Bukkit / Paper Hanger / Direct)
- Skips already downloaded files
- Password encryption for SFTP (AES-256-CBC)
- Optional SFTP upload support

View File

@@ -2,6 +2,9 @@
"global": {
"useragent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36",
"downloadPath": "downloads"
// Not in use but for future use
"MinecraftDistro": "Paper",
"MinecraftVersion": "latest" // Set to "latest" to always download the latest version, or specify a version like "1.20.4"
},
"urls": [
"https://github.com/McPlugin/SoCool",
@@ -12,7 +15,7 @@
"port": 22,
"username": "",
"password": "", // This will be encrypted by the script and removed from the file
"remotePath": "/minecraft/plugins",
"remotePath": "/minecraft/", // as of 1.1.0 you need to set this as the root of your minecraft server!
"privateKeyPath": "" // Leave blank if using password
}
}

256
index.js
View File

@@ -2,10 +2,10 @@ const axios = require("axios");
const fs = require("fs");
const path = require("path");
const { pipeline } = require("stream/promises");
const cliProgress = require("cli-progress");
const cheerio = require("cheerio");
const SftpClient = require("ssh2-sftp-client");
const crypto = require("crypto");
const pLimit = require("p-limit").default;
// --- Config & Secrets ---
const CONFIG_PATH = "config.json";
@@ -61,44 +61,49 @@ const ensureDir = (dir) => {
if (!fs.existsSync(dir)) fs.mkdirSync(dir);
};
const DOWNLOAD_PATH = config.global.downloadPath || "downloads";
ensureDir(DOWNLOAD_PATH);
const PLUGIN_PATH = path.join(DOWNLOAD_PATH, "Plugins");
const SERVEREXEC_PATH = path.join(DOWNLOAD_PATH, "Serverexec");
[DOWNLOAD_PATH, PLUGIN_PATH, SERVEREXEC_PATH].forEach(ensureDir);
const isDownloaded = (filename) => fs.existsSync(path.join(DOWNLOAD_PATH, filename));
const downloadJar = async (url, name) => {
if (isDownloaded(name)) {
const isServerJar = name === "server.jar";
const destDir = isServerJar ? SERVEREXEC_PATH : PLUGIN_PATH;
const filePath = path.join(destDir, name);
if (fs.existsSync(filePath)) {
console.log(`🟡 Skipped (already exists): ${name}`);
return;
}
const USER_AGENT = config.global?.userAgent || "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36";
const response = await axios.get(url, {
responseType: "stream",
headers: {
"User-Agent":
USER_AGENT,
},
});
const total = parseInt(response.headers["content-length"] || "0", 10);
const bar = new cliProgress.SingleBar({}, cliProgress.Presets.shades_classic);
bar.start(total, 0);
const response = await axios.get(url, { responseType: "stream" });
response.data.on("data", (chunk) => bar.increment(chunk.length));
response.data.on("end", () => bar.stop());
await pipeline(
response.data,
fs.createWriteStream(filePath)
);
const filePath = path.join(DOWNLOAD_PATH, name);
await pipeline(response.data, fs.createWriteStream(filePath));
console.log(`✔️ Saved: ${name}`);
};
// --- Source handlers ---
// --- Handle Jenkins ---
const handleJenkins = async (url) => {
const html = await axios.get(url).then((res) => res.data);
const $ = cheerio.load(html);
const links = $("a[href$='.jar']").map((_, el) => $(el).attr("href")).get();
const apiURL = url.endsWith("/")
? url + "api/json?tree=artifacts[fileName,relativePath]"
: url + "/api/json?tree=artifacts[fileName,relativePath]";
const data = await axios.get(apiURL).then((res) => res.data);
const links = data.artifacts
.filter((a) => a.fileName.endsWith(".jar"))
.map((a) => a.relativePath);
if (!links.length) throw new Error("No .jar files found on page");
const base = new URL(url);
const base = new URL(url + (url.endsWith("/") ? "" : "/") + "artifact/");
const preferred = ["paper", "spigot", "bukkit"];
const skip = ["javadoc", "sources", "cli", "bootstrap", "mojangapi", "nashorn", "remapper", "fabric", "neoforge"];
const essentialsOK = ["EssentialsX", "EssentialsXChat", "EssentialsXSpawn", "EssentialsXGeoIP"];
@@ -109,8 +114,8 @@ const handleJenkins = async (url) => {
const lower = fileName.toLowerCase();
if (skip.some((term) => lower.includes(term))) return null;
if (fileName.startsWith("EssentialsX")) {
const base = fileName.split("-")[0];
if (!essentialsOK.includes(base)) return null;
const baseName = fileName.split("-")[0];
if (!essentialsOK.includes(baseName)) return null;
}
return { href, fileName: lower };
})
@@ -131,6 +136,7 @@ const handleJenkins = async (url) => {
}
};
// --- Handle GitHub ---
const handleGitHub = async (url) => {
const match = url.match(/github\.com\/([^/]+\/[^/]+)/);
if (!match) throw new Error("Invalid GitHub URL format");
@@ -147,6 +153,7 @@ const handleGitHub = async (url) => {
await downloadJar(chosen.browser_download_url, chosen.name);
};
// --- Handle Modrinth ---
const handleModrinth = async (url) => {
const match = url.match(/modrinth\.com\/plugin\/([^/]+)/);
if (!match) throw new Error("Invalid Modrinth URL format");
@@ -156,13 +163,55 @@ const handleModrinth = async (url) => {
.get(`https://api.modrinth.com/v2/project/${project}/version`)
.then((res) => res.data);
const latest = versions[0];
const file = latest.files.find((f) => f.filename.endsWith(".jar"));
if (!file) throw new Error("No .jar file in latest version");
// Filter to only versions compatible with Spigot/Paper/etc
const compatible = versions.find((v) =>
(v.loaders || []).some((loader) =>
["spigot", "paper", "bukkit", "purpur", "folia"].includes(loader.toLowerCase())
)
);
if (!compatible) throw new Error("No compatible Spigot/Paper version found");
const file = compatible.files.find((f) => f.filename.endsWith(".jar"));
if (!file) throw new Error("No .jar file in compatible version");
await downloadJar(file.url, file.filename);
};
// --- Handle SpigotMC ---
const handleSpigotMC = async (url) => {
const match = url.match(/spigotmc\.org\/resources\/.*\.(\d+)/);
if (!match) throw new Error("Invalid SpigotMC resource URL");
const resourceId = match[1];
const resource = await axios
.get(`https://api.spiget.org/v2/resources/${resourceId}`)
.then(res => res.data);
const versionData = await axios
.get(`https://api.spiget.org/v2/resources/${resourceId}/versions/latest`)
.then(res => res.data);
const baseName = resource.name
.split("[")[0]
.trim()
.split(/\s+/)
.slice(0, 2)
.join("-");
const rawVersion = versionData?.name ?? "latest";
const version = rawVersion.replace(/^v/i, "");
const fileName = `${baseName}-${version}.jar`;
const downloadURL = `https://api.spiget.org/v2/resources/${resourceId}/download`;
console.log(`Spiget download: ${downloadURL}`);
await downloadJar(downloadURL, fileName);
};
// --- Handle Direct (Mainly for Floodgate) ---
const handleDirect = async (url) => {
let name = path.basename(url.split("?")[0]);
@@ -183,16 +232,21 @@ const handleDirect = async (url) => {
// --- Handle PaperMC ---
const handlePaperMC = async (url) => {
const versionMatch = url.match(/papermc.io\/download\/(.*?)(?:\/|$)/);
if (!versionMatch) throw new Error("Invalid PaperMC URL format");
const version = versionMatch[1];
if (url.includes("hangar.papermc.io")) {
const { data: html } = await axios.get(url);
const $ = cheerio.load(html);
const apiURL = `https://api.papermc.io/v2/projects/paper/versions/${version}/builds`;
const builds = await axios.get(apiURL).then((res) => res.data.builds);
const latestBuild = builds[0];
const jarLink = $('a[href$=".jar"]').first().attr("href");
if (!jarLink) throw new Error("❌ No .jar link found on Hangar page");
const downloadURL = latestBuild.downloads.application.url;
await downloadJar(downloadURL, `paper-${version}-${latestBuild.build}.jar`);
const downloadURL = jarLink.startsWith("http")
? jarLink
: `https://hangar.papermc.io${jarLink}`;
const fileName = path.basename(downloadURL);
await downloadJar(downloadURL, fileName);
return;
}
};
// --- Handle dev.bukkit.org ---
@@ -236,6 +290,26 @@ const handleBukkit = async (url) => {
await downloadJar(fullDownloadLink, filename);
};
// --- Handle PaperMC Server DL ---
const downloadLatestPaperMC = async () => {
console.log("📥 Downloading latest PaperMC server jar...");
const projectURL = 'https://api.papermc.io/v2/projects/paper';
const { data: versionsData } = await axios.get(projectURL);
const latestVersion = versionsData.versions[versionsData.versions.length - 1];
const buildsURL = `${projectURL}/versions/${latestVersion}`;
const { data: buildsData } = await axios.get(buildsURL);
const latestBuild = buildsData.builds[buildsData.builds.length - 1];
const jarInfoURL = `${projectURL}/versions/${latestVersion}/builds/${latestBuild}`;
const { data: jarInfo } = await axios.get(jarInfoURL);
const jarPath = jarInfo.downloads.application.name;
const downloadURL = `https://api.papermc.io/v2/projects/paper/versions/${latestVersion}/builds/${latestBuild}/downloads/${jarPath}`;
await downloadJar(downloadURL, "server.jar");
};
// --- Upload to SFTP ---
const uploadToSFTP = async () => {
@@ -246,8 +320,6 @@ const uploadToSFTP = async () => {
const sftp = new SftpClient();
const remote = sftpConfig.remotePath || "/";
const localFiles = fs.readdirSync(DOWNLOAD_PATH).filter(f => f.endsWith(".jar"));
const connectOptions = {
host: sftpConfig.host,
port: sftpConfig.port || 22,
@@ -268,29 +340,56 @@ const uploadToSFTP = async () => {
try {
await sftp.connect(connectOptions);
const remoteFiles = await sftp.list(remote);
const remoteJars = remoteFiles.filter(f => f.name.endsWith(".jar"));
for (const localFile of localFiles) {
const baseName = extractBaseName(localFile);
const toDelete = remoteJars.filter(remoteFile =>
extractBaseName(remoteFile.name) === baseName
);
for (const file of toDelete) {
const fullPath = path.posix.join(remote, file.name);
await sftp.delete(fullPath);
console.log(`🗑️ Deleted remote: ${file.name}`);
const uploadFolder = async (localDir, remoteDir) => {
const files = fs.readdirSync(localDir).filter(f => f.endsWith(".jar"));
const remoteFiles = await sftp.list(remoteDir);
const remoteJars = remoteFiles.filter(f => f.name.endsWith(".jar"));
for (const file of files) {
const baseName = extractBaseName(file);
const toDelete = remoteJars.filter(r => extractBaseName(r.name) === baseName);
for (const del of toDelete) {
const fullPath = path.posix.join(remoteDir, del.name);
await sftp.delete(fullPath);
console.log(`🗑️ Deleted remote: ${fullPath}`);
}
const localPath = path.join(localDir, file);
const remotePath = path.posix.join(remoteDir, file);
await sftp.fastPut(localPath, remotePath);
console.log(`⬆️ Uploaded to ${remoteDir}: ${file}`);
}
// 🚀 Upload new files
const localPath = path.join(DOWNLOAD_PATH, localFile);
const remotePath = path.posix.join(remote, localFile);
await sftp.fastPut(localPath, remotePath);
console.log(`⬆️ Uploaded: ${localFile}`);
}
};
await uploadFolder(PLUGIN_PATH, path.posix.join(remote, "plugins"));
await uploadFolder(SERVEREXEC_PATH, remote);
} catch (err) {
console.error("❌ SFTP Error:", err.message);
} finally {
sftp.end();
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error("Timeout closing SFTP connection")), 5000)
);
try {
await Promise.race([sftp.end(), timeout]);
console.log("🔌 SFTP connection closed.");
// Clean up local files after upload
const deleteFilesInDir = (dir) => {
fs.readdirSync(dir).forEach(file => {
const filePath = path.join(dir, file);
if (fs.lstatSync(filePath).isFile()) {
fs.unlinkSync(filePath);
console.log(`🧹 Deleted: ${filePath}`);
}
});
};
deleteFilesInDir(PLUGIN_PATH);
deleteFilesInDir(SERVEREXEC_PATH);
} catch (e) {
console.warn("⚠️ Error or timeout closing SFTP:", e.message);
}
}
};
@@ -304,28 +403,29 @@ const uploadToSFTP = async () => {
console.log(`🗑️ Deleted local file: ${file}`);
}
for (const url of config.urls) {
console.log(`\n📥 ${url}`);
try {
if (url.includes("github.com")) {
await handleGitHub(url);
} else if (url.includes("modrinth.com")) {
await handleModrinth(url);
} else if (url.includes("papermc.io")) {
await handlePaperMC(url);
} else if (url.includes("dev.bukkit.org")) {
await handleBukkit(url);
} else if (url.includes("/job/")) {
await handleJenkins(url);
} else if (url.endsWith(".jar") || url.includes("download.geysermc.org")) {
await handleDirect(url);
} else {
console.warn("⚠️ Skipping unknown URL format.");
await downloadLatestPaperMC();
console.log("\n🔍 Starting plugin downloads from configured URLs...");
const limit = pLimit(6);
await Promise.all(
config.urls.map(url =>
limit(async () => {
try {
if (url.includes("github.com")) return handleGitHub(url);
if (url.includes("modrinth.com")) return handleModrinth(url);
if (url.includes("papermc.io")) return handlePaperMC(url);
if (url.includes("dev.bukkit.org")) return handleBukkit(url);
if (url.includes("spigotmc.org")) return handleSpigotMC(url);
if (url.includes("/job/")) return handleJenkins(url);
if (url.endsWith(".jar") || url.includes("download.geysermc.org")) return handleDirect(url);
} catch (err) {
console.error(`❌ Failed: ${err.message}`);
}
} catch (err) {
console.error(`❌ Failed: ${err.message}`);
}
}
})
)
);
await uploadToSFTP();
})();

View File

@@ -1,6 +1,6 @@
{
"name": "download-plugs",
"version": "1.0.5",
"name": "super-cool-jar-downloader",
"version": "1.2.0",
"main": "index.js",
"scripts": {
"run": "node index.js"
@@ -12,7 +12,7 @@
"dependencies": {
"axios": "^1.9.0",
"cheerio": "^1.0.0",
"cli-progress": "^3.12.0",
"p-limit": "^7.2.0",
"ssh2-sftp-client": "^12.0.0"
}
}