Skip to content

Commit

Permalink
first
Browse files Browse the repository at this point in the history
  • Loading branch information
Wulian233 committed Jun 9, 2024
1 parent 6f4cfd6 commit 0e60d68
Show file tree
Hide file tree
Showing 10 changed files with 376 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
*.zip
/feedtheforge/__pycache__
/__main__.build
/__main__.dist
/__main__.onefile-build
FeedTheForge.exe
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Feed The Forge
## Introduce
This is a simple tool to download modpacks from FTB without the need of the FTB Launcher.

You can then import or drag this zip file into any curseforge compatible launcher.

For example: HMCL, PCL2, Prism Launcher etc.

## Usage
WIP

## Develop and Build
WIP

## LICENSE
[GNU General Public License v3.0](.LICENSE)
253 changes: 253 additions & 0 deletions __main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
import aiohttp
import asyncio
from urllib import request
from feedtheforge.const import *
import json
import os
import shutil
from zipfile import ZIP_DEFLATED, ZipFile
from pick import pick, Option

async def download_file(session, url, output_path):
async with session.get(url) as response:
with open(output_path, "wb") as f:
while chunk := await response.content.read(1024):
f.write(chunk)

async def _create_directory(path):
os.makedirs(path, exist_ok=True)

async def download_mod_files(session, non_curse_files):
tasks = []
for file_info in non_curse_files:
mod_file_path = file_info["path"][2:]
mod_file_name = file_info["name"]
full_path = os.path.join(modpack_path, "overrides", mod_file_path)
output_path = os.path.join(full_path, mod_file_name)

if not os.path.exists(output_path):
await _create_directory(full_path)
tasks.append(download_file(session, file_info["url"], output_path))

await asyncio.gather(*tasks)

async def _featured_and_search(load_json):
# 读取json并制作对应的选择菜单
options = []
with open(load_json, "r", encoding="utf-8") as f:
data = json.load(f)
for modpack_id in data["packs"]:
get_modpack_info(modpack_id)
with open(modpack_id_path, "r", encoding="utf-8") as pack_file:
pack_data = json.load(pack_file)
print(f"{pack_data['name']} (id {modpack_id})")
options.append(Option(f"{pack_data['name']}( id:{modpack_id})", modpack_id))
title = locale.t("feedtheforge.start.title")
modpack_id = pick(options, title, indicator="=>")
modpack_id = modpack_id[0].value
# 下载选择的整合包
await download_modpack(modpack_id)

async def get_featured_modpack():
featured_json = os.path.join(cache_dir, "featured_modpacks.json")
async with aiohttp.ClientSession() as session:
await download_file(session, api_featured, featured_json)

await _featured_and_search(featured_json)

async def search_modpack():
print("未完成,敬请期待")
# search_json = os.path.join(cache_dir, "search_modpacks.json")
# keyword = input(locale.t("feedtheforge.main.search_modpack"))
# async with aiohttp.ClientSession() as session:
# await download_file(session, api_search + keyword, search_json)

# await _featured_and_search(search_json)
# os.remove(search_json)

def get_modpack_info(modpack_id):
global modpack_id_path
modpack_id_path = os.path.join(cache_dir, f"pack-{modpack_id}.json")
with request.urlopen(f"https://api.modpacks.ch/public/modpack/{modpack_id}") as response:
data = json.loads(response.read().decode("utf-8"))

with open(modpack_id_path, "w", encoding="utf-8") as f:
f.write(json.dumps(data, indent=4))

async def chinese_patch(lanzou_url):
# 蓝奏云api直链解析下载
async with aiohttp.ClientSession() as session:
await download_file(session, f"https://tool.bitefu.net/lz?url={lanzou_url}", patch)
with ZipFile(patch, 'r') as zip_ref:
zip_ref.extractall(patch_folder)
os.remove(patch)
# 把汉化补丁移动剪切到整合包
for root, _, files in os.walk(patch_folder):
for file in files:
patch_file = os.path.join(root, file)
shutil.move(patch_file, modpack_path)
shutil.rmtree(patch_folder)

async def download_modpack(modpack_id):
get_modpack_info(modpack_id)
with open(modpack_id_path, "r", encoding="utf-8") as f:
modpack_data = json.load(f)

modpack_name = modpack_data["name"]
modpack_author = modpack_data["authors"][0]["name"]
modpack_version = modpack_data["versions"][0]["name"]
print(locale.t("feedtheforge.main.modpack_name", modpack_name = modpack_name))
versions = modpack_data["versions"]
version_list = [version["id"] for version in versions]
print(locale.t("feedtheforge.main.version_list", version_list = version_list))
selected_version = input(locale.t("feedtheforge.main.enter_version"))

# 输入为空且有版本可下载(更保险),取最新版本
if not selected_version and version_list:
selected_version = max(version_list)
print(locale.t("feedtheforge.main.default_version", selected_version = selected_version))
# id无效,无对应整合包
elif int(selected_version) not in version_list:
input(locale.t("feedtheforge.main.invalid_modpack_version"))
return
# 输入的不是数字,大错特错
else:
input(locale.t("feedtheforge.main.invalid_modpack_version"))
return

async with aiohttp.ClientSession() as session:
await download_file(session, f"https://api.modpacks.ch/public/modpack/{modpack_id}/{selected_version}",
os.path.join(cache_dir, "download.json"))
await get_modpack_files(modpack_name, modpack_author, modpack_version, session)
# 切片[-27:]恰为模组文件名
request.urlretrieve(i18nupdate_link, os.path.join(mod_path, i18nupdate_link[-27:]))
# 检查有无对应汉化
if str(selected_version) in all_patch:
install = input(locale.t("feedtheforge.main.has_chinese_patch"))
if install == "Y" or install == "y":
chinese_patch(selected_version, all_patch[selected_version])
else: pass
zip_modpack(modpack_name)

async def get_modpack_files(modpack_name, modpack_author, modpack_version, session):
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"]

curse_files, non_curse_files = [], []
for file_info in data["files"]:
if "curseforge" in file_info:
curse_files.append({
"fileID": file_info["curseforge"]["file"],
"projectID": file_info["curseforge"]["project"],
"required": True
})
else:
non_curse_files.append(file_info)

modloader_id = f"{modloader_name}-{modloader_version}"
if modloader_name == "neoforge" and mc_version == "1.20.1":
modloader_id = f"{modloader_name}-{mc_version}-{modloader_version}"

manifest_data = {
"author": modpack_author,
"files": curse_files,
"manifestType": "minecraftModpack",
"manifestVersion": 1,
"minecraft": {
"version": mc_version,
"modLoaders": [{"id": modloader_id, "primary": True}]
},
"name": modpack_name,
"overrides": "overrides",
"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)
os.makedirs(os.path.join(modpack_path, "overrides"), exist_ok=True)

await download_mod_files(session, non_curse_files)

def zip_modpack(modpack_name):
print(locale.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(locale.t("feedtheforge.main.modpack_created", modpack_name=f"{modpack_name}.zip"))
shutil.rmtree(modpack_path, ignore_errors=True)

def cleat_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(locale.t("feedtheforge.main.clean_temp", size=int(size/1024)))

async def get_modpack_list():
print(locale.t("feedtheforge.main.getting_list"))
try:
with request.urlopen(api_list) as response:
if response.status == 200:
modpacks_data = json.loads(response.read().decode("utf-8"))
with open(packlist_path, "w", encoding="utf-8") as f:
json.dump(modpacks_data, f, indent=4)
# 网络错误无法连接为OSError
except OSError:
input(locale.t("feedtheforge.main.getting_error"))
exit(1)

with open(packlist_path, "r", encoding="utf-8") as f:
modpacks_data = json.load(f)
global all_pack_ids
all_pack_ids = [str(all_pack_ids) for all_pack_ids in modpacks_data["packs"]]

async def main():
if not os.path.exists(cache_dir):
os.makedirs(cache_dir)

# 本地化中这里的字中间要有空格,不加空格VSCode终端正常,在cmd中字会重叠
title = locale.t("feedtheforge.start.title")
options = [
Option(locale.t("feedtheforge.start.featured_modpack"),
description=locale.t("feedtheforge.start.featured_modpack_desc")),
Option(locale.t("feedtheforge.start.search_modpack"),
description=locale.t("feedtheforge.start.search_modpack_desc")),
Option(locale.t("feedtheforge.start.enter_id"),
description=locale.t("feedtheforge.start.enter_id_desc")),
Option(locale.t("feedtheforge.start.clean_temp"),
description=locale.t("feedtheforge.start.clean_temp_desc"))
]
options, index = pick(options, title, indicator="=>")

if index == 0:
await get_featured_modpack()
elif index == 1:
await search_modpack()
elif index == 2:
await get_modpack_list()
modpack_id = input(locale.t("feedtheforge.main.enter_id"))
if modpack_id not in all_pack_ids:
print(locale.t("feedtheforge.main.invalid_pack_id"))
return
await download_modpack(modpack_id)
elif index == 3:
cleat_temp()

if __name__ == "__main__":
import sys
py_version = sys.version_info
# main.py L14; feedtheforge/i18n.py 类型标注为Python 3.8新功能
if py_version < (3, 8):
input(locale.t("feedtheforge.main.unsupported_version",
cur=f"{py_version.major}.{py_version.minor}.{py_version.micro}"))
exit(0)
asyncio.run(main())
33 changes: 33 additions & 0 deletions feedtheforge/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import os
import tempfile
from feedtheforge.i18n import Locale

# 默认且仅支持中文
locale = Locale("zh_cn")

cache_dir = os.path.join(tempfile.gettempdir(), "FeedTheForge")
packlist_path = os.path.join(cache_dir, "packlist.json")
modpack_path = os.path.join(cache_dir, "pack_files")

patch = os.path.join(cache_dir, "patch.zip")
patch_folder = os.path.join(cache_dir, "patch")
i18nupdate_link = "https://mediafilez.forgecdn.net/files/5335/196/I18nUpdateMod-3.5.5-all.jar"
mod_path = os.path.join(modpack_path, "overrides", "mods")

api_list = "https://api.modpacks.ch/public/modpack/all"
api_featured = "https://api.modpacks.ch/public/modpack/featured/20"
api_search = "https://api.modpacks.ch/public/modpack/search/20/detailed?platform=modpacksch&term="

# 全部汉化 key FTB唯一包版本 vaule 蓝奏云汉化下载链接
all_patch = {
# 100 StoneBlock 3
"6498": "https://wulian233.lanzouj.com/iwAZ61xg3yib",
"6647": "https://wulian233.lanzouj.com/iwAZ61xg3yib",
"6967": "https://wulian233.lanzouj.com/iwAZ61xg3yib",
"11655": "https://wulian233.lanzouj.com/iwAZ61xg3yib",
# 115 Arcanum Institute
"11512": "https://vmhanhuazu.lanzouo.com/i8W7Y1nr83le",
# 122 Builders Paradise 2
"11840": "https://wulian233.lanzouj.com/ib5G81wnrpwb",
"11937": "https://wulian233.lanzouj.com/ib5G81wnrpwb"
}
36 changes: 36 additions & 0 deletions feedtheforge/i18n.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from pathlib import Path
import json
from string import Template

class Locale:
def __init__(self, lang: str):
self.path = Path(f"./feedtheforge/lang/{lang}.json")
self.data = {}
self.load()

def __getitem__(self, key: str):
return self.data[key]

def __contains__(self, key: str):
return key in self.data

def load(self):
with open(self.path, "r", encoding="utf-8") as f:
d = f.read()
self.data = json.loads(d)
f.close()

def get_string(self, key: str, failed_prompt):
n = self.data.get(key, None)
if n != None:
return n
if failed_prompt:
return str(key) + self.t("feedtheforge.i18n.failed")
return key

def t(self, key: str, failed_prompt=True, *args, **kwargs):
localized = self.get_string(key, failed_prompt)
return Template(localized).safe_substitute(*args, **kwargs)


locale: Locale = Locale("zh_cn")
28 changes: 28 additions & 0 deletions feedtheforge/lang/zh_cn.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{ "feedtheforge.start.title": "按 上 下 键 选 择 , 回 车 确 认 :",
"feedtheforge.start.featured_modpack": "查 看 热 门 整 合 包",
"feedtheforge.start.featured_modpack_desc": "查 看 当 前 近 20天 最 热 门 的 5个 整 合 包 并 选 择 下 载",
"feedtheforge.start.search_modpack": "搜 索 整 合 包",
"feedtheforge.start.search_modpack_desc": "输 入 英 文 关 键 词 搜 索 整 合 包 , 选 择 后 下 载",
"feedtheforge.start.enter_id": "输 入 整 合 包 id",
"feedtheforge.start.enter_id_desc":"直 接 输 入 整 合 包 对 应 的 数 字 id下 载",
"feedtheforge.start.clean_temp": "清 除 工 具 缓 存",
"feedtheforge.start.clean_temp_desc": "清 除 缓 存 的 整 合 包 信 息 , 下 次 启 动 会 变 慢",

"feedtheforge.main.clean_temp": "清理缓存成功,共清理了 $size kb",
"feedtheforge.main.default_version": "已自动选择最新的 $selected_version 版本",
"feedtheforge.main.enter_id": "请输入要下载的整合包id:",
"feedtheforge.main.enter_version": "请输入整合包版本(留空默认最新):",
"feedtheforge.main.getting_error": "网络错误,获取整合包列表失败。回车自动退出",
"feedtheforge.main.getting_list": "正在获取整合包列表",
"feedtheforge.main.has_chinese_patch": "本整合包有人工汉化补丁可用,是否自动下载并安装?输入Y安装。",
"feedtheforge.main.search_modpack": "请输入整合包英文关键词:",
"feedtheforge.main.invalid_pack_id": "没有对应的整合包。请输入正确的整合包id",
"feedtheforge.main.invalid_modpack_version": "没有对应的整合包版本。请输入一个正确的版本。回车自动退出",
"feedtheforge.main.unsupported_version": "该程序需要Python 3.8+运行,当前版本为Python $cur 。回车自动退出",
"feedtheforge.main.version_list": "当前整合包可下载版本:$version_list",
"feedtheforge.main.modpack_created": "压缩成功。已创建名为 $modpack_name 的整合包,请拖入启动器安装",
"feedtheforge.main.modpack_name": "成功选择了 $modpack_name",
"feedtheforge.main.zipping_modpack": "下载完成,正在压缩制作整合包安装包。",

"feedtheforge.i18n.failed": "错误:没有对应的本地化字符串"
}
Binary file added icon.ico
Binary file not shown.
Binary file added icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
aiohttp
pick
2 changes: 2 additions & 0 deletions win_build.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pip install nuitka
nuitka --onefile --enable-console --enable-plugin=upx --show-progress --windows-icon-from-ico=.\icon.ico --output-file=FeedTheForge --include-data-dir=.\feedtheforge\lang=feedtheforge\lang __main__.py

0 comments on commit 0e60d68

Please sign in to comment.