Skip to content

Instantly share code, notes, and snippets.

@redzumi
Last active November 13, 2025 09:25
Show Gist options
  • Select an option

  • Save redzumi/ef7bda29bcd39977a32c6e0858aa7880 to your computer and use it in GitHub Desktop.

Select an option

Save redzumi/ef7bda29bcd39977a32c6e0858aa7880 to your computer and use it in GitHub Desktop.
Minecraft Modrinth Mods Downloader
import axios from "axios";
import fs from "fs";
import path from "path";
/** links.txt
https://modrinth.com/datapack/dungeons-and-taverns/versions?g=1.21.4&l=fabric
https://modrinth.com/datapack/dungeons+/versions?g=1.21.4&l=fabric
https://modrinth.com/datapack/explorify/versions?g=1.21.4&l=fabric
https://modrinth.com/datapack/hybrid-beta
*/
const links = fs.readFileSync(
path.join(import.meta.dirname, "links.txt"),
"utf8"
);
const projectIds = links
.split("\n")
.filter((link) => link.includes("modrinth.com"))
.map((link) => link.split("?")[0].replace("/versions", "").split("/").pop());
const targetGameVersion = "1.21.4";
const targetLoader = "fabric";
const getDownloadInfo = async (projectId) => {
const { versions, project } = await getProjectInfo(projectId);
const filteredVersions = versions.filter(
(version) =>
version.game_versions.includes(targetGameVersion) &&
version.loaders.includes(targetLoader)
);
const sortedVersions = filteredVersions.sort(
(a, b) => new Date(b.date_published) - new Date(a.date_published)
);
if (sortedVersions.length === 0) {
console.error(
`No ${targetGameVersion} ${targetLoader} version found for ${projectId}`
);
return { downloadUrl: null, downloadName: null, prefix: null };
}
return {
downloadUrl: sortedVersions[0].files[0].url,
downloadName: sortedVersions[0].files[0].filename,
prefix: getPrefix(project.client_side, project.server_side),
};
};
const getVersions = async (projectId) => {
const baseUrl = "https://api.modrinth.com/v2/";
const response = await axios.request({
method: "get",
url: `${baseUrl}project/${projectId}/version`,
headers: {},
});
return response.data;
};
const getProject = async (projectId) => {
const baseUrl = "https://api.modrinth.com/v2/";
const response = await axios.request({
method: "get",
url: `${baseUrl}project/${projectId}`,
headers: {},
});
return response.data;
};
const getProjectInfo = async (projectId) => {
const [versions, , project] = await Promise.all([
getVersions(projectId),
sleep(1000),
getProject(projectId),
]);
return { versions, project };
};
/**
* 'c' - is client only, should be only in 'mods' on client side
* 'cs' - both sides, should be in 'mods' for server and client side
*/
const getPrefix = (client_side, server_side) => {
if (server_side === "unsupported") {
return "c";
}
return "cs";
};
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
for (const projectId of await projectIds) {
const index = projectIds.indexOf(projectId);
const { downloadUrl, downloadName, prefix } = await getDownloadInfo(
projectId
);
if (downloadUrl === null) {
console.error(`No download URL found for ${projectId}`);
continue;
}
const downloadResponse = await axios.get(downloadUrl, {
responseType: "stream",
});
const writer = fs.createWriteStream(
path.join(import.meta.dirname, "mods", `${prefix}_${downloadName}`)
);
downloadResponse.data.pipe(writer);
writer.on("finish", () => {
console.log(`Downloaded ${downloadName}`);
});
writer.on("error", (error) => {
console.error(`Error downloading ${downloadName}: ${error}`);
});
await sleep(1000);
console.log(`Downloaded ${projectId}`);
console.log(`${index + 1} of ${projectIds.length} downloaded`);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment