因为我的世界这个游戏很神奇,每次一更新就得亲自把mod一个一个的下完,而mojang的更新也太会水版本号了,所以就想做个帮我扫描mod目录下的mod,然后帮我下载相应版本的python小程序。
天影大侠他满心壮志地查modrinth怎么用api下mod,他们是怎么识别mod,然后不小心问了问manus...
算了manus直接把我想要的给做了出来
自己再改些没用的,就发出来了。
大家试试
llm太神奇了,给我玩mc节省了好多时间
python做的,用之前先pip install asyncio aiohttp tqdm,仅在linux上测试
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# 感谢llm的帮忙
# 引入一些东西
import os
import asyncio
import aiohttp
from tqdm import tqdm
import platform
import zipfile
import json
import hashlib# 颜色
C_RED = "\033[91m";
C_GREEN = "\033[92m";
C_YELLOW = "\033[93m";
C_BLUE = "\033[94m";
C_CYAN = "\033[96m";
C_MAGENTA = "\033[95m";
C_RESET = "\033[0m";
C_BOLD = "\033[1m"
def print_color(message, color ): print(f"{color}{message}{C_RESET}")
def print_title(message): print_color(f"\n{'='*10} {message} {'='*10}", f"{C_CYAN}{C_BOLD}")
def print_success(message): print_color(f"✓ {message}", C_GREEN)
def print_warning(message): print_color(f"⚠️ {message}", C_YELLOW)
def print_error(message): print_color(f"✗ {message}", C_RED)
def print_info(message): print_color(f"ℹ️ {message}", C_BLUE)
def print_local(message): print_color(f"Found: {message}", C_MAGENTA)class ModUpdater:
MODRINTH_API_URL = "https://api.modrinth.com/v2"
USER_AGENT = "一个mod下载工具"
PATHS_FILE = "mod_paths.json"
TQDM_BAR_FORMAT = "{l_bar}{bar:20}| {n_fmt}/{total_fmt}"def __init__(self, mc_version, mod_loader, mod_folder ):
self.mc_version = mc_version
self.mod_loader = mod_loader
self.mod_folder = mod_folder
self.download_dir = f"{self.mc_version}-{self.mod_loader}"
os.makedirs(self.download_dir, exist_ok=True)@staticmethod
def load_paths():
if not os.path.exists(ModUpdater.PATHS_FILE): return []
try:
with open(ModUpdater.PATHS_FILE, 'r', encoding='utf-8') as f: return json.load(f)
except (json.JSONDecodeError, IOError): return []@staticmethod
def save_paths(paths):
try:
with open(ModUpdater.PATHS_FILE, 'w', encoding='utf-8') as f: json.dump(paths, f, indent=2)
except IOError as e: print_error(f"无法保存路径文件: {e}")def _calculate_sha1(self, filepath):
sha1 = hashlib.sha1()
with open(filepath, 'rb') as f:
while chunk := f.read(8192):
sha1.update(chunk)
return sha1.hexdigest()async def _api_request(self, session, method, url, **kwargs):
try:
async with session.request(method, url, **kwargs) as resp:
if resp.status == 200: return await resp.json()
except aiohttp.ClientError: pass
return Noneasync def _get_project_info(self, session, project_id ):
url = f"{self.MODRINTH_API_URL}/project/{project_id}"
return await self._api_request(session, "GET", url)async def _download_file(self, session, version_id, pbar):
try:
url = f"{self.MODRINTH_API_URL}/version/{version_id}"
version_data = await self._api_request(session, "GET", url)
if not version_data: raise ValueError("无法获取版本信息")file_info = version_data["files"][0]
download_url, name = file_info["url"], file_info["filename"]
path = os.path.join(self.download_dir, name)async with session.get(download_url) as r:
r.raise_for_status()
with open(path, "wb") as f:
while True:
chunk = await r.content.read(8192)
if not chunk: break
f.write(chunk)
pbar.update(1)
return name, True
except Exception:
pbar.update(1)
return None, Falseasync def run(self):
print_info(f"模组将被下载到: ./{self.download_dir}/")
print_title("1. 扫描并识别本地模组")
jar_files = [os.path.join(self.mod_folder, f) for f in os.listdir(self.mod_folder) if f.endswith(".jar")]
hashes = [self._calculate_sha1(f) for f in tqdm(jar_files, desc="计算哈希", bar_format=self.TQDM_BAR_FORMAT, ascii=' -')]local_projects = {}
async with aiohttp.ClientSession(headers={"User-Agent": self.USER_AGENT} ) as session:
url = f"{self.MODRINTH_API_URL}/version_files"
version_data = await self._api_request(session, "POST", url, json={"hashes": hashes, "algorithm": "sha1"})
if not version_data:
print_error("无法从 Modrinth API 获取任何模组信息。")
returnprint_info("从你选择的mod文件夹中找到以下模组:")
project_ids = {info['project_id'] for info in version_data.values()}
project_infos = await asyncio.gather(*[self._get_project_info(session, pid) for pid in project_ids])
for info in project_infos:
if info:
local_projects[info['id']] = info
print_local(info['title'])unidentified_hashes = [h for h in hashes if h not in version_data]
print_title(f"2. 下载 {len(local_projects)} 个mod")
version_tasks = []
for pid in local_projects.keys():
url = f"{self.MODRINTH_API_URL}/project/{pid}/version"
params = {"loaders": f'["{self.mod_loader}"]', "game_versions": f'["{self.mc_version}"]'}
version_tasks.append(self._api_request(session, "GET", url, params=params))
versions_results = await asyncio.gather(*version_tasks)
download_tasks, failed_to_find_version = [], []
project_ids_list = list(local_projects.keys())
for i, versions in enumerate(versions_results):
pid = project_ids_list[i]
if versions:
download_tasks.append(versions[0]['id'])
else:
failed_to_find_version.append(local_projects[pid]['title'])download_results = []
if download_tasks:
with tqdm(total=len(download_tasks), desc="下载中", bar_format=self.TQDM_BAR_FORMAT, ascii=' -') as pbar:
tasks_to_run = [self._download_file(session, vid, pbar) for vid in download_tasks]
download_results = await asyncio.gather(*tasks_to_run)print_title("3. 下载完成")
successful = [name for name, success in download_results if success]
failed_titles = [info['title'] for (pid, info), (name, success) in zip(local_projects.items(), download_results) if not success]
failed_titles.extend(failed_to_find_version)print_success(f"成功下载: {len(successful)} 个")
if failed_titles:
print_error(f"下载失败或未找到兼容版本: {len(set(failed_titles))} 个")
for mod in sorted(list(set(failed_titles))): print(f" - {mod}")
if unidentified_hashes:
print_warning(f"无法识别 {len(unidentified_hashes)} 个 .jar 文件")def get_user_mod_folder():
paths = ModUpdater.load_paths()
print(f"{C_YELLOW}请选择一个模组文件夹路径:{C_RESET}")
for i, path in enumerate(paths): print(f" {i}. {path}")
new_path_index = len(paths)
print(f" {new_path_index}. 手动输入新路径")choice = -1
while not (0 <= choice <= new_path_index):
try: choice = int(input(f"{C_YELLOW}请输入您的选择 (0-{new_path_index}): {C_RESET}"))
except ValueError: choice = -1if choice == new_path_index:
new_path = input(f"{C_YELLOW}请输入新的模组文件夹路径: {C_RESET}")
if os.path.isdir(new_path):
if new_path not in paths:
paths.append(new_path)
ModUpdater.save_paths(paths)
return new_path
else:
print_error(f"错误: 路径 '{new_path}' 不存在或不是一个文件夹。")
return None
else:
return paths[choice]async def main():
print_title("一个mod下载工具")
mod_folder = get_user_mod_folder()
if not mod_folder: returnmc_version = input(f"{C_YELLOW}请输入 Minecraft 版本 (例如 1.20.1): {C_RESET}")
print(f"{C_YELLOW}请选择 Mod 加载器:{C_RESET}")
loaders = ["fabric", "forge", "neoforge", "quilt"]
for i, loader in enumerate(loaders, 1): print(f" {i}. {loader.capitalize()}")
choice = 0
while not 1 <= choice <= len(loaders):
try: choice = int(input(f"{C_YELLOW}请输入您的选择 (1-{len(loaders)}): {C_RESET}"))
except ValueError: choice = 0
mod_loader = loaders[choice - 1]updater = ModUpdater(mc_version, mod_loader, mod_folder)
await updater.run()if __name__ == "__main__":
if platform.system() == "Windows":
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
try:
asyncio.run(main())
except KeyboardInterrupt:
print_warning("\n程序被中断。")
finally:
print_title("程序结束")