diff --git a/instances/forge-1.20.1-survival/download_mods.py b/instances/forge-1.20.1-survival/download_mods.py deleted file mode 100755 index dbe3342..0000000 --- a/instances/forge-1.20.1-survival/download_mods.py +++ /dev/null @@ -1,231 +0,0 @@ -#!/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()