diff --git a/.gitattributes b/.gitattributes index d0c0c4c..d4f2ad7 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,3 @@ * text eol=lf *.png binary +*.ico binary \ No newline at end of file diff --git a/README.md b/README.md index 368bb33..7be0624 100644 --- a/README.md +++ b/README.md @@ -15,27 +15,27 @@ WIP ## Develop and Build ### Requirements -- **Git** -- **Python Version**: 3.8+ +- **[Git](https://git-scm.com/downloads)** +- **[Python](https://www.python.org/downloads/)**: 3.8+ - **Supported Operating Systems**: Windows 10 or later, macOS, Linux ### Running from Source 1. **Install Dependencies**: - - Open a terminal and run the following command to install required packages: + - Open a terminal and run: ```bash git clone https://github.com/Wulian233/FeedTheForge.git cd FeedTheForge pip install -r requirements.txt ``` -2. **Run the Application**: - - **Windows**: Use the command `python __main__.py` - - **macOS and Linux**: Use the command `python3 __main__.py` +2. **Run**: + - **Windows**: `python __main__.py` + - **macOS and Linux**: `python3 __main__.py` -### Building Executable +### Package as Executable -1. **Package as Executable**: +1. **Package**: ```bash pip install pyinstaller pyinstaller PyBuild/main.spec diff --git a/feedtheforge/async_downloader.py b/feedtheforge/async_downloader.py index b1c0c81..35a696e 100644 --- a/feedtheforge/async_downloader.py +++ b/feedtheforge/async_downloader.py @@ -1,22 +1,42 @@ import aiohttp +import aiofiles +import asyncio class AsyncDownloader: - def __init__(self): + def __init__(self, retries=2, retry_delay=2): self.session = None + self.retries = retries + self.retry_delay = retry_delay async def __aenter__(self): - self.session = await aiohttp.ClientSession().__aenter__() + timeout = aiohttp.ClientTimeout(total=10) # 设置总超时 + self.session = await aiohttp.ClientSession( + connector=aiohttp.TCPConnector(ssl=False), + timeout=timeout + ).__aenter__() return self async def __aexit__(self, exc_type, exc, tb): await self.session.__aexit__(exc_type, exc, tb) async def download_file(self, url, output_path): - async with self.session.get(url) as response: - with open(output_path, "wb") as f: - while chunk := await response.content.read(1024): - f.write(chunk) + attempts = 0 + while attempts < self.retries: + try: + async with self.session.get(url) as response: + async with aiofiles.open(output_path, "wb") as f: + async for chunk in response.content.iter_chunked(1024*64): + await f.write(chunk) + print(f"文件下载成功: {output_path}") + break # 下载成功,退出循环 + except Exception: + attempts += 1 + if attempts >= self.retries: + print(f"文件下载失败,跳过: {output_path}") + else: + print(f"下载失败,尝试重新下载 ({attempts}/{self.retries})...") + await asyncio.sleep(self.retry_delay) async def fetch_json(self, url): async with self.session.get(url) as response: - return await response.json() \ No newline at end of file + return await response.json() diff --git a/feedtheforge/i18n.py b/feedtheforge/i18n.py index bcb2718..1e6b757 100644 --- a/feedtheforge/i18n.py +++ b/feedtheforge/i18n.py @@ -11,7 +11,7 @@ def __init__(self, lang: str): self.path = Path(sys._MEIPASS) / f"feedtheforge/lang/{lang}.json" else: # If running in a normal Python environment - self.path = Path(f"./feedtheforge/lang/{lang}.py") + self.path = Path(f"./feedtheforge/lang/{lang}.json") self.data = {} self.load() diff --git a/feedtheforge/utils.py b/feedtheforge/utils.py index 7d05826..75d2372 100644 --- a/feedtheforge/utils.py +++ b/feedtheforge/utils.py @@ -1,66 +1,68 @@ -import os -import shutil -from zipfile import ZIP_DEFLATED, ZipFile - -from feedtheforge.const import * - - -async def create_directory(path): - """ - 创建目录,如果目录不存在则创建 - """ - os.makedirs(path, exist_ok=True) - -async def is_recent_file(filepath, days=7): - """ - 检查文件最后修改时间是否在指定天数内 - - :param filepath: 文件路径 - :param days: 距离当前的天数间隔,默认值为7天 - :return: 如果文件存在且最后修改时间在指定天数内,返回 True 否则返回 False - """ - from datetime import datetime, timedelta - - if not os.path.exists(filepath): - return False - modification_date = datetime.fromtimestamp(os.path.getmtime(filepath)).date() - current_date = datetime.now().date() - if current_date - modification_date < timedelta(days=days): - return True - return False - -def zip_modpack(modpack_name): - """ - 压缩整合包文件夹为一个zip文件 - - :param modpack_name: 整合包的名称 - """ - print(lang.t("feedtheforge.main.zipping_modpack")) - - with ZipFile(f"{modpack_name}.zip", "w", ZIP_DEFLATED) as zf: - for dirname, _, files in os.walk(modpack_path): - for filename in files: - file_path = os.path.join(dirname, filename) - zf.write(file_path, os.path.relpath(file_path, modpack_path)) - - print(lang.t("feedtheforge.main.modpack_created", modpack_name=f"{modpack_name}.zip")) - shutil.rmtree(modpack_path, ignore_errors=True) - -def clean_temp(): - """ - 清理缓存目录中的临时文件 - """ - size = 0 - for root, _, files in os.walk(cache_dir): - size += sum([os.path.getsize(os.path.join(root, name)) for name in files]) - shutil.rmtree(cache_dir, ignore_errors=True) - print(lang.t("feedtheforge.main.clean_temp", size=int(size / 1024))) - -def pause(): - """ - 退出程序 - """ - if os.name == 'nt': - os.system('pause') - else: - input(lang.t("feedtheforge.main.pause")) +import os +import shutil + +from feedtheforge.const import * + + +async def create_directory(path): + """ + 创建目录,如果目录不存在则创建 + """ + os.makedirs(path, exist_ok=True) + +async def is_recent_file(filepath, days=7): + """ + 检查文件最后修改时间是否在指定天数内 + + :param filepath: 文件路径 + :param days: 距离当前的天数间隔,默认值为7天 + :return: 如果文件存在且最后修改时间在指定天数内,返回 True 否则返回 False + """ + from datetime import datetime, timedelta + + if not os.path.exists(filepath): + return False + modification_date = datetime.fromtimestamp(os.path.getmtime(filepath)).date() + current_date = datetime.now().date() + if current_date - modification_date < timedelta(days=days): + return True + return False + +def zip_modpack(modpack_name): + """ + 压缩整合包文件夹为一个zip文件 + + :param modpack_name: 整合包的名称 + """ + from zipfile import ZIP_DEFLATED, ZipFile + + print(lang.t("feedtheforge.main.zipping_modpack")) + + with ZipFile(f"{modpack_name}.zip", "w", ZIP_DEFLATED) as zf: + for dirname, _, files in os.walk(modpack_path): + for filename in files: + file_path = os.path.join(dirname, filename) + zf.write(file_path, os.path.relpath(file_path, modpack_path)) + + print(lang.t("feedtheforge.main.modpack_created", modpack_name=f"{modpack_name}.zip")) + shutil.rmtree(modpack_path, ignore_errors=True) + +def clean_temp(): + """ + 清理缓存目录中的临时文件 + """ + size = 0 + for root, _, files in os.walk(cache_dir): + size += sum([os.path.getsize(os.path.join(root, name)) for name in files]) + shutil.rmtree(cache_dir, ignore_errors=True) + print(lang.t("feedtheforge.main.clean_temp", size=int(size / 1024))) + +def pause(): + """ + 退出程序 + """ + if os.name == 'nt': + os.system('pause') + else: + input(lang.t("feedtheforge.main.pause")) + exit(0) \ No newline at end of file diff --git a/main.py b/main.py index be0db58..e5207e5 100644 --- a/main.py +++ b/main.py @@ -2,6 +2,7 @@ import json import os import shutil +import aiofiles from pick import pick, Option from feedtheforge import utils @@ -34,16 +35,16 @@ async def load_modpack_data(modpack_id: str) -> dict: async with AsyncDownloader() as dl: data = await dl.fetch_json(url) - with open(modpack_id_path, "w", encoding="utf-8") as f: - json.dump(data, f, indent=4) + async with aiofiles.open(modpack_id_path, "w", encoding="utf-8") as f: + await f.write(json.dumps(data, indent=4)) return data async def display_modpack_list(load_json): """读取json并制作对应的选择菜单""" options = [] - with open(load_json, "r", encoding="utf-8") as f: - data = json.load(f) + async with aiofiles.open(load_json, "r", encoding="utf-8") as f: + data = json.loads(await f.read()) for modpack in data["packs"]: # 处理字典形式的数据,即搜索整合包 if isinstance(modpack, dict): modpack_id = modpack['id'] @@ -100,7 +101,7 @@ async def apply_chinese_patch(lanzou_url: str) -> None: from zipfile import ZipFile # 获取返回的json中downUrl的值为下载链接 data = json.loads(LanzouDownloader().get_direct_link(lanzou_url)) - down_url = data.get("downUrl") + down_url = data.get("downUrl") async with AsyncDownloader() as dl: await dl.download_file(down_url, patch) @@ -119,10 +120,11 @@ async def apply_chinese_patch(lanzou_url: str) -> None: async def download_modpack(modpack_id: str) -> None: - print(lang.t("feedtheforge.main.modpack_name", modpack_name=modpack_name)) - modpack_data = await load_modpack_data(modpack_id) modpack_name = modpack_data["name"] + + print(lang.t("feedtheforge.main.modpack_name", modpack_name=modpack_name)) + modpack_author = modpack_data["authors"][0]["name"] versions = modpack_data["versions"] version_list = [version["id"] for version in versions] @@ -138,11 +140,10 @@ async def download_modpack(modpack_id: str) -> None: print(lang.t("feedtheforge.main.invalid_modpack_version")) utils.pause() - async with AsyncDownloader() as dl: download_url = f"https://api.modpacks.ch/public/modpack/{modpack_id}/{selected_version}" await dl.download_file(download_url, os.path.join(cache_dir, "download.json")) - await prepare_modpack_files(modpack_name, modpack_author, selected_version) + await prepare_modpack_files(modpack_name, modpack_author) if current_language == "zh_CN": async with AsyncDownloader() as dl: @@ -157,15 +158,12 @@ async def download_modpack(modpack_id: str) -> None: utils.zip_modpack(modpack_name) -async def prepare_modpack_files(modpack_name, modpack_author, modpack_version): +async def prepare_modpack_files(modpack_name, modpack_author): os.makedirs(modpack_path, exist_ok=True) - with open(os.path.join(cache_dir, "download.json"), "r", encoding="utf-8") as f: - data = json.load(f) - # 下面均为CurseForge整合包识别的固定格式 - mc_version = data["targets"][1]["version"] - modloader_name = data["targets"][0]["name"] - modloader_version = data["targets"][0]["version"] - + async with aiofiles.open(os.path.join(cache_dir, "download.json"), "r", encoding="utf-8") as f: + data = json.loads(await f.read()) + + # 下面均为CurseForge整合包识别的固定格式 curse_files, non_curse_files = [], [] for file_info in data["files"]: if "curseforge" in file_info: @@ -177,7 +175,11 @@ async def prepare_modpack_files(modpack_name, modpack_author, modpack_version): else: non_curse_files.append(file_info) + mc_version = data["targets"][1]["version"] + modloader_name = data["targets"][0]["name"] + modloader_version = data["targets"][0]["version"] modloader_id = f"{modloader_name}-{modloader_version}" + modpack_version = data["name"] if modloader_name == "neoforge" and mc_version == "1.20.1": modloader_id = f"{modloader_name}-{mc_version}-{modloader_version}" @@ -195,17 +197,17 @@ async def prepare_modpack_files(modpack_name, modpack_author, modpack_version): "version": modpack_version } - with open(os.path.join(modpack_path, "manifest.json"), "w", encoding="utf-8") as f: - json.dump(manifest_data, f, indent=4) + async with aiofiles.open(os.path.join(modpack_path, "manifest.json"), "w", encoding="utf-8") as f: + await f.write(json.dumps(manifest_data, indent=4)) # FIXME 生成 modlist.html 文件 modlist_file = os.path.join(modpack_path, "modlist.html") - with open(modlist_file, "w", encoding="utf-8") as f: - f.write("