前言

Mojang太会刷版本号了,而每一次更新都要重新把自己想用的mod全部下载一遍
为了节省时间,就做了这个东西
找的mauns和deepseek帮忙
只支持modrinth的mod下载,因为courseforge弄不来
支持forge,fabric,quilt,neoforge

代码

使用前请先

pip install asyncio aiohttp tqdm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
# 引入一些东西
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"

# print
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 None

async 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, False

async 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 获取任何模组信息。")
return

print_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 = -1

if 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: return

mc_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("程序结束")

使用方法

使用python打开,然后输入之前的版本的mod文件夹路径(添加了输入的路径保留功能的)
选择mod加载器
输入版本
它就会下载到你启动该python程序目录下的1.21.1-fabric等文件夹里
但有时还是有些mod下不到,只能一定程度上方便一些。