Last active
June 16, 2024 07:13
-
-
Save Yoplitein/e335e4f20775409af70d579f3dfa60da to your computer and use it in GitHub Desktop.
Script to download CurseForge modpacks
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from urllib import request | |
import io | |
import json | |
import os | |
import sys | |
import time | |
import urllib | |
import zipfile | |
def main(): | |
if len(sys.argv) < 2: | |
print(f"Usage: {sys.argv[0]} <pack zip URL>") | |
raise SystemExit(1) | |
packZipUrl = sys.argv[1] | |
if "curseforge.com/api/v1/mods" not in packZipUrl: | |
print('Pack zip URL should look like "https://www.curseforge.com/api/v1/mods/..."') | |
print('(copy the manual download link, labeled "try again")') | |
raise SystemExit(1) | |
os.makedirs("mods", exist_ok = True) | |
print("Downloading pack zip") | |
with fetch(packZipUrl) as rawZip: | |
rawZip = io.BytesIO(rawZip.read()) | |
with zipfile.ZipFile(rawZip) as zip: | |
print("Parsing manifest") | |
manifest = None | |
with zip.open("manifest.json", "r") as f: | |
manifest = json.load(f) | |
print("Downloading mods") | |
for file in manifest["files"]: | |
project = file["projectID"] | |
file = file["fileID"] | |
filename = downloadJar(project, file) | |
print(f"=> downloaded {filename}") | |
print("Unzipping config") | |
configs = (f for f in zip.namelist() if f.startswith("overrides/")) | |
for file in configs: | |
path = file.split("/", 1)[1] | |
dirname = os.path.dirname(path) | |
if dirname != "": | |
os.makedirs(os.path.dirname(path), exist_ok = True) | |
with zip.open(file, "r") as src: | |
with open(path, "wb") as dst: | |
pipeFile(src, dst) | |
print(f"=> unzipped {path}") | |
mcInfo = manifest["minecraft"] | |
print(f"\nMinecraft version {mcInfo['version']}") | |
print("Recommended modloader version(s):") | |
for loader in mcInfo["modLoaders"]: | |
print(f"* {loader['id']}") | |
print("\nDone!") | |
headers = { | |
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; rv:117.0) Gecko/20100101 Firefox/117.0" | |
} | |
def fetch(url): | |
attempts = 5 | |
for _ in range(attempts): | |
try: | |
req = request.Request(url, headers = headers) | |
return request.urlopen(req, timeout = 10) | |
except urllib.error.URLError as err: | |
print(f"retrying... ({err})") | |
time.sleep(1) | |
pass | |
raise TimeoutError(f"Fetching `{url}` timed out after {attempts} attempts") | |
def downloadJar(project, file): | |
url = f"https://www.curseforge.com/api/v1/mods/{project}/files/{file}/download" | |
with fetch(url) as resp: | |
(_, filename) = resp.url.rsplit("/", 1) | |
path = f"mods/{filename}" | |
if os.path.exists(path): | |
print("...already downloaded") | |
return filename | |
with open(f"mods/{filename}", "wb") as f: | |
pipeFile(resp, f) | |
return filename | |
pipeBuf = bytearray(64 * 1024) | |
def pipeFile(src, dst): | |
while True: | |
len = src.readinto(pipeBuf) | |
if len > 0: | |
dst.write(pipeBuf[:len]) | |
else: | |
break | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment