mod download script

This commit is contained in:
JaniSoto 2026-08-10 23:33:57 +00:00
parent 4327d6a828
commit 7018f1d80f
63 changed files with 231 additions and 0 deletions

View file

@ -0,0 +1,231 @@
#!/usr/bin/env python3
import json
import os
import re
import urllib.parse
import urllib.request
from concurrent.futures import ThreadPoolExecutor, as_completed
# Configuration
TARGET_DIR = "./data/mods"
MC_VERSION = "1.20.1"
LOADER = "forge"
MAX_WORKERS = 8
USER_AGENT = "MCServerModDownloader/2.0 (admin@server.local)"
# Explicit Slug Overrides for Modrinth
SLUG_OVERRIDES = {
"perspatium": "ad-astra-per-spatium-et-tempus",
"planets+": "ad-astra-planets",
"ad_astra_more_structures": "ad-astra-more-structures",
"alexsmobs": "alexs-mobs",
"architectury": "architectury-api",
"cloth-config": "cloth-config",
"create_ad_astra_recipes": "create-ad-astra-recipes",
"ferritecore": "ferrite-core",
"flan": "flan",
"improvedmobs": "improved-mobs",
"jei": "jei",
"patchouli": "patchouli",
"player-animation-lib": "playeranimator",
"resourcefulconfig": "resourceful-config",
"resourcefullib": "resourceful-lib",
"simplebackups": "simple-backups",
"sophisticatedcore": "sophisticated-core",
"tectonic": "tectonic",
"spark": "spark",
"tenshilib": "tenshilib",
"voicechat": "simple-voice-chat",
"yungsapi": "yungs-api",
"yungsbetterdungeons": "yungs-better-dungeons",
"yungsbettermineshafts": "yungs-better-mineshafts",
"yungsbetterstrongholds": "yungs-better-strongholds",
}
SERVER_MODS = [
"[forge]ctov-3.4.14.jar",
"ad_astra-forge-1.20.1-1.15.20.jar",
"ad_astra_extra_additions-1.20.1-1.1.1.jar",
"ad_astra_more_structures-1.0.1-forge.jar",
"alexsmobs-1.22.9.jar",
"architectury-9.2.14-forge.jar",
"bettercombat-forge-1.9.0+1.20.1.jar",
"Botania-1.20.1-454-FORGE.jar",
"botarium-forge-1.20.1-2.3.4.jar",
"citadel-2.6.3-1.20.1.jar",
"cloth-config-11.1.136-forge.jar",
"Clumps-forge-1.20.1-12.0.0.4.jar",
"connectivity-1.20.1-7.6.jar",
"corpse-forge-1.20.1-1.0.23.jar",
"create-1.20.1-6.0.8.jar",
"create_ad_astra_recipes-1.0.0-forge-1.20.1.jar",
"createaddition-1.20.1-1.3.3.jar",
"cupboard-1.20.1-3.7.jar",
"curios-forge-5.14.1+1.20.1.jar",
"decocraft-3.0.4-1.20.1-slim.jar",
"DistantHorizons-2.1.2-a-1.20.1.jar",
"FarmersDelight-1.20.1-1.3.2.jar",
"ferritecore-6.0.1-forge.jar",
"flan-1.20.1-1.11.16-forge.jar",
"geckolib-forge-1.20.1-4.8.4.jar",
"ImmersiveEngineering-1.20.1-10.2.0-183.jar",
"improvedmobs-1.20.1-1.13.7-forge.jar",
"irons_lib-1.20.1-2.1.0.jar",
"irons_spellbooks-1.20.1-3.16.2.jar",
"Jade-1.20.1-Forge-11.13.3.jar",
"jei-1.20.1-forge-15.48.0.180.jar",
"journeymap-forge-1.20.1-6.0.0.jar",
"Krypton Reno-forge-1.20.1-26.1.1-1.20.1.jar",
"lithostitched-forge-1.20.1-1.4.11.jar",
"locks_reforged-1.6.1.jar",
"lts_auth-1.0.1+mc1.20.1.jar",
"LuckPerms-Forge-5.4.102.jar",
"majrusz-library-forge-1.20.1-7.0.8.jar",
"majruszs-difficulty-forge-1.20.1-1.9.10.jar",
"mcda-5.0.2.jar",
"mcdw-9.0.4.jar",
"minecraft-comes-alive-7.6.28-beta.10+1.20.1-universal.jar",
"modernfix-forge-5.27.66+mc1.20.1.jar",
"Nullscape_1.20.x_v1.2.8.jar",
"Patchouli-1.20.1-85-FORGE.jar",
"perspatium-1.20.1-1.2.0.jar",
"planets+-bv1.7.5-1.20x.jar",
"player-animation-lib-forge-1.0.2-rc1+1.20.jar",
"resourcefulconfig-forge-1.20.1-2.1.3.jar",
"resourcefullib-forge-1.20.1-2.1.29.jar",
"SimpleBackups-1.20.1-3.1.24.jar",
"sophisticatedbackpacks-1.20.1-3.24.63.2049.jar",
"sophisticatedcore-1.20.1-1.3.75.2231.jar",
"spark-1.10.53-forge.jar",
"tectonic-3.0.17-forge-1.20.1.jar",
"tenshilib-1.20.1-1.7.6-forge.jar",
"Terralith_1.20.x_v2.5.4.jar",
"voicechat-forge-1.20.1-2.6.22.jar",
"YungsApi-1.20-Forge-4.0.6.jar",
"YungsBetterDungeons-1.20-Forge-4.0.4.jar",
"YungsBetterMineshafts-1.20-Forge-4.0.4.jar",
"YungsBetterStrongholds-1.20-Forge-4.0.3.jar",
]
def clean_mod_name(filename):
"""Strip prefixed metadata and digits to extract purely the mod identifier."""
name = re.sub(r"^\[.*?\]", "", filename)
name = re.sub(r"\.jar$", "", name)
# Split tokens by hyphen/underscore and keep tokens before version digits
tokens = re.split(r"[-_]", name)
clean_tokens = []
for token in tokens:
if re.match(r"^(v?\d|bv?\d|forge|fabric|quilt|mc1|1\.20|universal|slim)", token, re.I):
break
clean_tokens.append(token)
result = "-".join(clean_tokens).strip("-")
return result if result else name
def http_get_json(url):
req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
with urllib.request.urlopen(req, timeout=10) as resp:
return json.loads(resp.read().decode("utf-8"))
def download_file(url, target_path):
req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
with urllib.request.urlopen(req, timeout=30) as resp, open(target_path, "wb") as f:
f.write(resp.read())
def process_mod(jar_name):
target_path = os.path.join(TARGET_DIR, jar_name)
if os.path.exists(target_path):
return ("EXISTS", jar_name, "File already in destination")
raw_clean = clean_mod_name(jar_name)
slug = SLUG_OVERRIDES.get(raw_clean.lower(), raw_clean.lower())
project_id = None
# Step 1: Try direct project lookup by slug
try:
proj_data = http_get_json(f"https://api.modrinth.com/v2/project/{slug}")
if proj_data and "id" in proj_data:
project_id = proj_data["id"]
except Exception:
pass
# Step 2: Fallback search if direct slug lookup fails
if not project_id:
try:
search_q = slug.replace("-", " ")
facets = json.dumps([["categories:" + LOADER], ["versions:" + MC_VERSION]])
query_url = f"https://api.modrinth.com/v2/search?query={urllib.parse.quote(search_q)}&facets={urllib.parse.quote(facets)}"
data = http_get_json(query_url)
hits = data.get("hits", [])
if hits:
project_id = hits[0]["project_id"]
except Exception:
pass
if not project_id:
return ("NOT_FOUND", jar_name, f"Could not resolve Modrinth project for '{slug}'")
# Step 3: Fetch versions and download
try:
ver_url = f"https://api.modrinth.com/v2/project/{project_id}/version?loaders=[%22{LOADER}%22]&game_versions=[%22{MC_VERSION}%22]"
versions = http_get_json(ver_url)
if not versions:
return ("NOT_FOUND", jar_name, f"No {LOADER} {MC_VERSION} release on Modrinth")
download_url = None
for ver in versions:
for f in ver.get("files", []):
if f.get("filename") == jar_name:
download_url = f.get("url")
break
if download_url:
break
if not download_url and versions[0].get("files"):
download_url = versions[0]["files"][0].get("url")
if download_url:
download_file(download_url, target_path)
return ("SUCCESS", jar_name, "Downloaded from Modrinth")
else:
return ("NOT_FOUND", jar_name, "No download URL available")
except Exception as e:
return ("ERROR", jar_name, str(e))
def main():
os.makedirs(TARGET_DIR, exist_ok=True)
print(f"🚀 Starting download for {len(SERVER_MODS)} mods into '{TARGET_DIR}'...")
print(f"⚙️ Target: Minecraft {MC_VERSION} ({LOADER.upper()}) | Workers: {MAX_WORKERS}\n")
results = {"SUCCESS": [], "EXISTS": [], "NOT_FOUND": [], "ERROR": []}
with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
futures = {executor.submit(process_mod, mod): mod for mod in SERVER_MODS}
for future in as_completed(futures):
status, jar_name, msg = future.result()
results[status].append((jar_name, msg))
symbol = "✅" if status == "SUCCESS" else "⏭️" if status == "EXISTS" else "❌"
print(f"{symbol} [{status}] {jar_name} -> {msg}")
print("\n" + "=" * 60)
print("📊 DOWNLOAD SUMMARY")
print("=" * 60)
print(f" ✅ Downloaded: {len(results['SUCCESS'])}")
print(f" ⏭️ Skipped: {len(results['EXISTS'])}")
print(f" ❌ Not Found: {len(results['NOT_FOUND']) + len(results['ERROR'])}")
missing = results['NOT_FOUND'] + results['ERROR']
if missing:
print("\n⚠️ The following mods were not found automatically:")
for jar_name, msg in missing:
print(f" • {jar_name} ({msg})")
if __name__ == "__main__":
main()