mmccli 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. mmccli-0.1.0/PKG-INFO +69 -0
  2. mmccli-0.1.0/README.md +51 -0
  3. mmccli-0.1.0/pyproject.toml +34 -0
  4. mmccli-0.1.0/pyproject.toml.orig +33 -0
  5. mmccli-0.1.0/src/mmccli/__init__.py +1 -0
  6. mmccli-0.1.0/src/mmccli/add.py +327 -0
  7. mmccli-0.1.0/src/mmccli/app.py +227 -0
  8. mmccli-0.1.0/src/mmccli/bin/forge-install-bootstrapper.jar +0 -0
  9. mmccli-0.1.0/src/mmccli/bin/whats this.txt +6 -0
  10. mmccli-0.1.0/src/mmccli/cache/__init__.py +19 -0
  11. mmccli-0.1.0/src/mmccli/cache/_lib.py +47 -0
  12. mmccli-0.1.0/src/mmccli/fabric/__init__.py +6 -0
  13. mmccli-0.1.0/src/mmccli/fabric/install.py +43 -0
  14. mmccli-0.1.0/src/mmccli/fabric/model.py +72 -0
  15. mmccli-0.1.0/src/mmccli/fabric/versions.py +35 -0
  16. mmccli-0.1.0/src/mmccli/forge/__init__.py +12 -0
  17. mmccli-0.1.0/src/mmccli/forge/install.py +67 -0
  18. mmccli-0.1.0/src/mmccli/forge/model.py +106 -0
  19. mmccli-0.1.0/src/mmccli/forge/promos.py +146 -0
  20. mmccli-0.1.0/src/mmccli/log/__init__.py +34 -0
  21. mmccli-0.1.0/src/mmccli/log/error.py +4 -0
  22. mmccli-0.1.0/src/mmccli/log/gateway.py +8 -0
  23. mmccli-0.1.0/src/mmccli/log/logger.py +23 -0
  24. mmccli-0.1.0/src/mmccli/log/state.py +12 -0
  25. mmccli-0.1.0/src/mmccli/minecraft/__init__.py +42 -0
  26. mmccli-0.1.0/src/mmccli/minecraft/_model.py +53 -0
  27. mmccli-0.1.0/src/mmccli/minecraft/app.py +66 -0
  28. mmccli-0.1.0/src/mmccli/minecraft/locate.py +0 -0
  29. mmccli-0.1.0/src/mmccli/minecraft/manifests.py +85 -0
  30. mmccli-0.1.0/src/mmccli/minecraft/model.py +89 -0
  31. mmccli-0.1.0/src/mmccli/minecraft/validate.py +38 -0
  32. mmccli-0.1.0/src/mmccli/ms_oauth/__init__.py +12 -0
  33. mmccli-0.1.0/src/mmccli/ms_oauth/_model.py +9 -0
  34. mmccli-0.1.0/src/mmccli/ms_oauth/error.py +4 -0
  35. mmccli-0.1.0/src/mmccli/ms_oauth/login.py +96 -0
  36. mmccli-0.1.0/src/mmccli/ms_oauth/model.py +31 -0
  37. mmccli-0.1.0/src/mmccli/path_dector.py +28 -0
mmccli-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,69 @@
1
+ Metadata-Version: 2.3
2
+ Name: mmccli
3
+ Version: 0.1.0
4
+ Summary: Minecraft Java Edition Manager in Commandline.
5
+ Author: MangoFanFanw
6
+ Author-email: MangoFanFanw <mangofanfanw@icloud.com>
7
+ Requires-Dist: certifi>=2026.10.19
8
+ Requires-Dist: click>=8.5.0
9
+ Requires-Dist: dotenv>=0.9.9
10
+ Requires-Dist: msal>=1.39.0
11
+ Requires-Dist: pydantic>=2.14.1
12
+ Requires-Dist: pypdl>=1.5.7
13
+ Requires-Dist: requests>=2.34.2
14
+ Requires-Dist: rich>=15.0.0
15
+ Requires-Dist: typer>=0.27.3
16
+ Requires-Python: >=3.14
17
+ Description-Content-Type: text/markdown
18
+
19
+ # Minecraft \(Java Edition Manager in\) Commandline
20
+
21
+ > Minecraft Java 版命令行管理工具,简写为 MCLI,命令为 mmccli。
22
+ >
23
+ > ~因为 mcli 已经重名了……~
24
+
25
+ 这是一个使用 Python 实现的命令行工具,用于管理 Minecraft Java 版的版本实例。
26
+
27
+ 或者简单来说是一个下载器。
28
+
29
+ **MCLI 不是一款 Minecraft 启动器,你仍然需要一个启动器来启动本工具添加的版本。**
30
+
31
+ ## 特色
32
+
33
+ MCLI 致力于实现类似于 *包管理器*的使用体验。
34
+
35
+ * 依赖库文件、资源文件等全局缓存,在游戏目录中硬链接。
36
+ * 多任务、多线程并发下载,提高下载速度(也许)。
37
+ * 可配置镜像源,提高下载速度(也许)。
38
+
39
+ 并且不只是下载游戏本体,MCLI 还支持:
40
+
41
+ * 添加模组加载器。
42
+ * 已支持 1.13+ Forge、Fabric,启动器可识别与启动。
43
+ * 正在计划支持 NeoForge 和 Quilt(这个还在更新吗?)
44
+
45
+ 未来 MCLI 还长期计划:
46
+
47
+ * 支持一行命令从 CurseForge 和/或 Modrinth 安装模组。
48
+ * 支持一行命令安装服务器。
49
+ * 使用更快速的方式重新实现下载功能。
50
+
51
+ ## 使用
52
+
53
+ ### 安装(或者不安装)
54
+
55
+ ```shell
56
+ uvx mmccli <command>
57
+
58
+ # 或者安装为工具,然后直接使用
59
+ uv tool install mmccli
60
+ mmccli <command>
61
+ ```
62
+
63
+ ### 添加游戏
64
+
65
+ ```shell
66
+ mmccli add --minecraft 26.1
67
+ mmccli add --minecraft 26.2 --forge latest
68
+ mmccli add --minecraft 26.3 --name "原版 26.3"
69
+ ```
mmccli-0.1.0/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # Minecraft \(Java Edition Manager in\) Commandline
2
+
3
+ > Minecraft Java 版命令行管理工具,简写为 MCLI,命令为 mmccli。
4
+ >
5
+ > ~因为 mcli 已经重名了……~
6
+
7
+ 这是一个使用 Python 实现的命令行工具,用于管理 Minecraft Java 版的版本实例。
8
+
9
+ 或者简单来说是一个下载器。
10
+
11
+ **MCLI 不是一款 Minecraft 启动器,你仍然需要一个启动器来启动本工具添加的版本。**
12
+
13
+ ## 特色
14
+
15
+ MCLI 致力于实现类似于 *包管理器*的使用体验。
16
+
17
+ * 依赖库文件、资源文件等全局缓存,在游戏目录中硬链接。
18
+ * 多任务、多线程并发下载,提高下载速度(也许)。
19
+ * 可配置镜像源,提高下载速度(也许)。
20
+
21
+ 并且不只是下载游戏本体,MCLI 还支持:
22
+
23
+ * 添加模组加载器。
24
+ * 已支持 1.13+ Forge、Fabric,启动器可识别与启动。
25
+ * 正在计划支持 NeoForge 和 Quilt(这个还在更新吗?)
26
+
27
+ 未来 MCLI 还长期计划:
28
+
29
+ * 支持一行命令从 CurseForge 和/或 Modrinth 安装模组。
30
+ * 支持一行命令安装服务器。
31
+ * 使用更快速的方式重新实现下载功能。
32
+
33
+ ## 使用
34
+
35
+ ### 安装(或者不安装)
36
+
37
+ ```shell
38
+ uvx mmccli <command>
39
+
40
+ # 或者安装为工具,然后直接使用
41
+ uv tool install mmccli
42
+ mmccli <command>
43
+ ```
44
+
45
+ ### 添加游戏
46
+
47
+ ```shell
48
+ mmccli add --minecraft 26.1
49
+ mmccli add --minecraft 26.2 --forge latest
50
+ mmccli add --minecraft 26.3 --name "原版 26.3"
51
+ ```
@@ -0,0 +1,34 @@
1
+ [project]
2
+ name = "mmccli"
3
+ version = "0.1.0"
4
+ description = "Minecraft Java Edition Manager in Commandline."
5
+ readme = "README.md"
6
+ requires-python = ">=3.14"
7
+ dependencies = [
8
+ "certifi>=2026.10.19",
9
+ "click>=8.5.0",
10
+ "dotenv>=0.9.9",
11
+ "msal>=1.39.0",
12
+ "pydantic>=2.14.1",
13
+ "pypdl>=1.5.7",
14
+ "requests>=2.34.2",
15
+ "rich>=15.0.0",
16
+ "typer>=0.27.3",
17
+ ]
18
+
19
+ [[project.authors]]
20
+ name = "MangoFanFanw"
21
+ email = "mangofanfanw@icloud.com"
22
+
23
+ [project.scripts]
24
+ mmccli = "mmccli:app"
25
+
26
+ [build-system]
27
+ requires = ["uv_build>=0.13.4"]
28
+ build-backend = "uv_build"
29
+
30
+ [dependency-groups]
31
+ dev = [
32
+ "mypy>=2.4.0",
33
+ "ruff>=0.17.1",
34
+ ]
@@ -0,0 +1,33 @@
1
+ [project]
2
+ name = "mmccli"
3
+ version = "0.1.0"
4
+ description = "Minecraft Java Edition Manager in Commandline."
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "MangoFanFanw", email = "mangofanfanw@icloud.com" }
8
+ ]
9
+ requires-python = ">=3.14"
10
+ dependencies = [
11
+ "certifi>=2026.10.19",
12
+ "click>=8.5.0",
13
+ "dotenv>=0.9.9",
14
+ "msal>=1.39.0",
15
+ "pydantic>=2.14.1",
16
+ "pypdl>=1.5.7",
17
+ "requests>=2.34.2",
18
+ "rich>=15.0.0",
19
+ "typer>=0.27.3",
20
+ ]
21
+
22
+ [project.scripts]
23
+ mmccli = "mmccli:app"
24
+
25
+ [build-system]
26
+ requires = ["uv_build>=0.13.4"]
27
+ build-backend = "uv_build"
28
+
29
+ [dependency-groups]
30
+ dev = [
31
+ "mypy>=2.4.0",
32
+ "ruff>=0.17.1",
33
+ ]
@@ -0,0 +1 @@
1
+ from .app import app
@@ -0,0 +1,327 @@
1
+ # mmccli.minecraft.add
2
+ import json
3
+ import os
4
+ import ssl
5
+ import time
6
+ from pathlib import Path
7
+ from typing import Literal
8
+
9
+ import certifi
10
+ import requests
11
+ from pypdl import Pypdl
12
+ from pypdl.utils import EFuture, FileValidator
13
+ from rich.progress import Progress
14
+
15
+ from mmccli.cache import (
16
+ get_assets_cache_file_path,
17
+ get_forge_installer_cache_file_path,
18
+ get_library_cache_file_path,
19
+ get_version_jar_cache_file_path,
20
+ link_or_move,
21
+ )
22
+ from mmccli.forge import ForgeInstallerVersionInfo
23
+ from mmccli.forge.model import ForgeInstallerInstallProfileJson, ForgeInstallerVersionJson
24
+ from mmccli.log import RequestStatusError, console, out, warn
25
+ from mmccli.minecraft import (
26
+ McGameAssetIndexJson,
27
+ McGameVersionJson,
28
+ McGameVersionJson_FileModel,
29
+ McGameVersionJson_LibrariesItem,
30
+ McLauncherLaunchProfilesJson,
31
+ McLauncherLaunchProfilesJson_ProfilesItem,
32
+ validate_assets,
33
+ validate_libraries,
34
+ )
35
+
36
+ ssl_context = ssl.create_default_context(cafile=certifi.where())
37
+
38
+ COPY_DICT = dict[str | Path, str | Path]
39
+
40
+
41
+ def load_mc_version_json(version_json_url: str) -> McGameVersionJson:
42
+ response = requests.get(version_json_url)
43
+ out(version_json_url)
44
+ data = response.json()
45
+ if not response.ok:
46
+ raise RequestStatusError(f"获取游戏版本 version.json 时请求失败 | {version_json_url=}")
47
+ return McGameVersionJson.model_validate(data)
48
+
49
+
50
+ def load_assetsIndex_json(assetIndex_json_url: str) -> McGameAssetIndexJson:
51
+ response = requests.get(assetIndex_json_url)
52
+ out(assetIndex_json_url)
53
+ data = response.json()
54
+ if not response.ok:
55
+ raise RequestStatusError(f"获取游戏资源索引 assetIndex.json 时请求失败 | {assetIndex_json_url=}")
56
+ return McGameAssetIndexJson.model_validate(data)
57
+
58
+
59
+ def download_libraries(
60
+ libraries: list[McGameVersionJson_LibrariesItem],
61
+ dl: Pypdl,
62
+ libraries_from: Literal["minecraft", "forge", "fabric"],
63
+ ):
64
+ if libraries_from == "minecraft":
65
+ base_url = os.environ.get("MCLI_MC_GAME_DOWNLOAD_LIBRARIES_URL", None)
66
+ template_url_prefix = "https://libraries.minecraft.net/"
67
+ elif libraries_from == "forge":
68
+ base_url = os.environ.get("MCLI_FORGE_DOWNLOAD_LIBRARIES_URL", None)
69
+ template_url_prefix = "https://maven.minecraftforge.net/"
70
+ elif libraries_from == "fabric":
71
+ base_url = os.environ.get("MCLI_FABRIC_DOWNLOAD_LIBRARIES_URL", None)
72
+ template_url_prefix = "https://maven.fabricmc.net/"
73
+ else:
74
+ raise RuntimeError(f"Unexpected value {libraries_from=}")
75
+
76
+ # 并非所有库文件都要下载,此处存放需要下载的库文件
77
+ download_list: list[McGameVersionJson_LibrariesItem] = []
78
+
79
+ for item in libraries:
80
+ if item.rules is None: # 没有 rules 的 library
81
+ download_list.append(item)
82
+ continue
83
+
84
+ for rule in item.rules:
85
+ if rule.action == "allow" and rule.os and rule.os.name == "windows":
86
+ download_list.append(item)
87
+
88
+ # 拼装任务列表,批量下载
89
+ tasks = []
90
+ copy_dict: COPY_DICT = {}
91
+ total_size = 0
92
+ for item in download_list:
93
+ item: McGameVersionJson_LibrariesItem # type: ignore[no-redef]
94
+ if item.downloads.artifact.url == "": # 为什么会存在 library 的 url 为空呢?
95
+ continue # 原来这个玩意是不需要手动下载的呀!
96
+ file_path = Path.cwd() / ".minecraft" / "libraries" / item.downloads.artifact.path # 游戏目录中的库文件路径
97
+ file_path.parent.mkdir(exist_ok=True, parents=True)
98
+ cache_file_path = get_library_cache_file_path(item.downloads.artifact.path) # 缓存中的库文件路径
99
+ cache_file_path.parent.mkdir(exist_ok=True, parents=True)
100
+ copy_dict[file_path] = cache_file_path # 建立关联
101
+ if cache_file_path.exists():
102
+ continue # 信任缓存,跳过缓存命中
103
+ if item.downloads.artifact.size:
104
+ total_size += item.downloads.artifact.size
105
+ url = (
106
+ item.downloads.artifact.url.replace(template_url_prefix, base_url)
107
+ if base_url
108
+ else item.downloads.artifact.url
109
+ )
110
+ out(url)
111
+ tasks.append(
112
+ {
113
+ "url": url,
114
+ "file_path": str(cache_file_path),
115
+ "retries": 2,
116
+ "segments": 8 if item.downloads.artifact.size and item.downloads.artifact.size >= 2_000_000 else 2,
117
+ "mirrors": item.downloads.artifact.url if base_url else None,
118
+ }
119
+ )
120
+ out(f"待下载库文件数量 | {len(tasks)=}")
121
+
122
+ if len(tasks) > 0:
123
+ results = _multi_download_with_progress(tasks, total_size, dl)
124
+ validate_libraries(download_list, results)
125
+ out("链接或复制文件……")
126
+
127
+ for file_path, cache_file_path in copy_dict.items(): # type: ignore[assignment]
128
+ link_or_move(cache_file_path, file_path)
129
+ out("库文件已下载完成。")
130
+
131
+
132
+ def download_assets(assetIndex_data: McGameAssetIndexJson, dl: Pypdl):
133
+ # 拼装任务列表,批量下载
134
+ tasks = []
135
+ copy_dict: COPY_DICT = {}
136
+ total_size = 0
137
+ base_url = os.environ.get("MCLI_MC_GAME_DOWNLOAD_ASSETS_URL", None)
138
+ for item in assetIndex_data.objects.values():
139
+ file_path = Path.cwd() / ".minecraft" / "assets" / "objects" / item.hash[:2] / item.hash
140
+ file_path.parent.mkdir(exist_ok=True, parents=True)
141
+ cache_file_path = get_assets_cache_file_path(f"objects/{item.hash[:2]}/{item.hash}")
142
+ cache_file_path.parent.mkdir(exist_ok=True, parents=True)
143
+ copy_dict[file_path] = cache_file_path
144
+ if cache_file_path.exists():
145
+ continue # 信任缓存,跳过缓存命中
146
+ total_size += item.size
147
+ path = item.hash[:2] + "/" + item.hash
148
+ url = (base_url if base_url else "https://resources.download.minecraft.net/") + path
149
+ out(url)
150
+ tasks.append(
151
+ {
152
+ "url": url,
153
+ "file_path": str(cache_file_path),
154
+ "retries": 2,
155
+ "segments": 4 if item.size >= 1_000_000 else 1,
156
+ "mirrors": ("https://resources.download.minecraft.net/" + path) if base_url else None,
157
+ }
158
+ )
159
+ out(f"待下载资源文件数量 | {len(tasks)=}")
160
+
161
+ if len(tasks) > 0:
162
+ results = _multi_download_with_progress(tasks, total_size, dl)
163
+ validate_assets(list(assetIndex_data.objects.values()), results)
164
+ out("链接或复制文件……")
165
+
166
+ for file_path, cache_file_path in copy_dict.items(): # type: ignore[assignment]
167
+ link_or_move(cache_file_path, file_path)
168
+ out("资源文件已下载完成。")
169
+
170
+
171
+ def download_mc_version_jar(version_id: str, clientItem: McGameVersionJson_FileModel, dl: Pypdl, name: str):
172
+ file_path = Path.cwd() / ".minecraft" / "versions" / name / (name + ".jar")
173
+ file_path.parent.mkdir(exist_ok=True, parents=True)
174
+ cache_file_path = get_version_jar_cache_file_path("client/" + version_id + ".jar")
175
+ cache_file_path.parent.mkdir(exist_ok=True, parents=True)
176
+ base_url = os.environ.get("MCLI_MC_GAME_DOWNLOAD_VERSIONS_URL", None)
177
+ if not cache_file_path.exists(): # 信任缓存
178
+ result = _single_download_with_progress(
179
+ url=clientItem.url.replace("https://piston-data.mojang.com/", base_url) if base_url else clientItem.url,
180
+ mirrors=clientItem.url if base_url else None,
181
+ file_path=str(cache_file_path),
182
+ size=clientItem.size,
183
+ dl=dl,
184
+ )
185
+ if result[0][1].get_hash("sha1") != clientItem.sha1:
186
+ warn("下载的版本游戏主文件 version.jar 存在损坏。")
187
+ out("链接或复制文件……")
188
+
189
+ link_or_move(cache_file_path, file_path)
190
+ out("版本 jar 游戏主文件下载完成。")
191
+
192
+
193
+ def download_forge_installer_jar(info: ForgeInstallerVersionInfo, dl: Pypdl):
194
+ cache_file_path = get_forge_installer_cache_file_path(info.name)
195
+ cache_file_path.parent.mkdir(exist_ok=True, parents=True)
196
+ if not cache_file_path.exists(): # 信任缓存
197
+ result = _single_download_with_progress(
198
+ url=f"https://bmclapi2.bangbang93.com/forge/download/{info.build}",
199
+ file_path=str(cache_file_path),
200
+ dl=dl,
201
+ )
202
+ if result[0][1].get_hash("sha1") != info.hash:
203
+ warn("下载的 Forge 安装器文件存在损坏。")
204
+
205
+ return cache_file_path
206
+
207
+
208
+ def analysis_forge_installer_version_json(file_path: str):
209
+ from tempfile import TemporaryDirectory
210
+ from zipfile import ZipFile
211
+
212
+ with TemporaryDirectory() as tmp_dir:
213
+ tmp_path = Path(tmp_dir)
214
+ with ZipFile(file=file_path, mode="r") as z:
215
+ z.extractall(tmp_path / "forge-installer-extractall")
216
+
217
+ with open(file=tmp_path / "forge-installer-extractall" / "version.json") as f:
218
+ version_json = ForgeInstallerVersionJson.model_validate_json(f.read())
219
+
220
+ with open(file=tmp_path / "forge-installer-extractall" / "install_profile.json") as f:
221
+ install_profile_json = ForgeInstallerInstallProfileJson.model_validate_json(f.read())
222
+
223
+ out("已在临时目录解压并解析 Forge 安装器内的 version.json 和 install_profile.json")
224
+ return version_json, install_profile_json
225
+
226
+
227
+ def save_mc_version_json(version_json: McGameVersionJson, name: str):
228
+ file_path = Path.cwd() / ".minecraft" / "versions" / name / (name + ".json")
229
+ file_path.parent.mkdir(exist_ok=True, parents=True)
230
+ with open(file=file_path, mode="w", encoding="UTF-8") as f:
231
+ f.write(version_json.model_dump_json(exclude_unset=True, indent=4))
232
+ out("已保存版本 json 文件。")
233
+
234
+
235
+ def save_assetIndex_json(assetIndex_id: str, assetIndex_json: McGameAssetIndexJson):
236
+ file_path = Path.cwd() / ".minecraft" / "assets" / "indexes" / (assetIndex_id + ".json")
237
+ file_path.parent.mkdir(exist_ok=True, parents=True)
238
+ with open(file=file_path, mode="w", encoding="UTF-8") as f:
239
+ f.write(assetIndex_json.model_dump_json(exclude_unset=True, indent=4))
240
+ out("已保存资源文件索引 json 文件。")
241
+
242
+
243
+ def save_launcher_profiles_json(mc_version_json: McGameVersionJson):
244
+ from datetime import datetime
245
+ from uuid import uuid4
246
+
247
+ key = uuid4().hex
248
+ now = datetime.now()
249
+
250
+ # 已有启动器配置文件就写入,否则新建
251
+ file_path = Path.cwd() / ".minecraft" / "launcher_profiles.json"
252
+ file_path.parent.mkdir(exist_ok=True, parents=True)
253
+ profile = McLauncherLaunchProfilesJson_ProfilesItem(
254
+ created=now.isoformat(),
255
+ icon="Grass",
256
+ javaArgs="-Xms2G -Xmx4G -XX:+UseCompactObjectHeaders -XX:+AlwaysPreTouch -XX:+UseStringDeduplication -XX:+UseZGC",
257
+ lastUsed="1970-01-01T00:00:00.000Z",
258
+ lastVersionId=mc_version_json.id,
259
+ name=mc_version_json.id,
260
+ type="custom",
261
+ )
262
+ if file_path.exists():
263
+ with open(file=file_path, mode="r") as f:
264
+ launcher_profiles = McLauncherLaunchProfilesJson.model_validate_json(f.read())
265
+ launcher_profiles.profiles["key"] = profile
266
+ else:
267
+ launcher_profiles = McLauncherLaunchProfilesJson(
268
+ profiles={key: profile},
269
+ settings={},
270
+ version=6,
271
+ )
272
+ with open(file=file_path, mode="w", encoding="UTF-8") as f:
273
+ f.write(launcher_profiles.model_dump_json(indent=4))
274
+ out("已更新或新建启动器配置 json 文件。")
275
+
276
+
277
+ def _multi_download_with_progress(tasks: list[dict], total_size: int, dl: Pypdl) -> list[tuple[str, FileValidator]]:
278
+ future: EFuture = dl.start(
279
+ tasks=tasks,
280
+ block=False,
281
+ hash_algorithms="sha1",
282
+ display=False,
283
+ proxy=os.environ.get("HTTP_PROXY", None),
284
+ overwrite=False,
285
+ ssl=ssl_context,
286
+ )
287
+
288
+ if len(tasks) > 0:
289
+ with Progress(console=console, transient=True) as progress:
290
+ task_files = progress.add_task("[cyan]下载总文件", total=dl.total_task)
291
+ task_size = progress.add_task("[green]下载总空间", total=total_size)
292
+ while not dl.completed:
293
+ progress.update(task_files, completed=dl.completed_task)
294
+ progress.update(task_size, completed=dl.current_size)
295
+ time.sleep(0.5)
296
+
297
+ return future.result() # type: ignore[no-any-return]
298
+
299
+
300
+ def _single_download_with_progress(
301
+ url: str,
302
+ file_path: str,
303
+ dl: Pypdl,
304
+ mirrors: str | list[str] | None = None,
305
+ size: int | None = None,
306
+ ) -> list[tuple[str, FileValidator]]:
307
+ future: EFuture = dl.start(
308
+ url=url,
309
+ file_path=file_path,
310
+ mirrors=mirrors,
311
+ segments=8,
312
+ retries=2,
313
+ block=False,
314
+ overwrite=False,
315
+ hash_algorithms="sha1",
316
+ display=False,
317
+ proxy=os.environ.get("HTTP_PROXY", None),
318
+ ssl=ssl_context,
319
+ )
320
+
321
+ with Progress(console=console, transient=True) as progress:
322
+ task_size = progress.add_task("[green]下载空间", total=size)
323
+ while not dl.completed:
324
+ progress.update(task_size, completed=dl.current_size)
325
+ time.sleep(0.5)
326
+
327
+ return future.result() # type: ignore[no-any-return]