-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.py
222 lines (170 loc) · 6.91 KB
/
main.py
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
216
217
218
219
220
221
222
import os
from pathlib import Path
import json
from util.prettyprint import pp_print as pp, pp as pp_format
from util.json_to_dataclass import parse as json_to_obj, add_module
from util.modelstruct import pydefault
from util.webparser import WebJSONExtractor
import zipfile
import glob
import requests
try:
from progress.bar import Bar as ProgressBar # type: ignore
except:
try:
from pip._vendor.progress.bar import Bar as ProgressBar # type: ignore
except:
from pip._vendor.rich.progress import Bar as ProgressBar # type: ignore
import model
add_module(model)
FILEPATH = "local/"
def parse_website_recursive(obj):
if not isinstance(obj, dict):
return None
if 'provider' in obj:
return obj
for v in obj.values():
ret = parse_website_recursive(v)
if ret is not None:
return ret
return None
def parse_website(content):
extractor = WebJSONExtractor()
extractor.feed(content)
assert extractor.content is not None
data = extractor.content or None
if data is None:
return None
data = json.loads(data)
return parse_website_recursive(data)
def download_pack_meta(url: 'str|None'):
if url:
with requests.get(url) as req:
req.raise_for_status()
if not req.encoding:
req.encoding = 'utf-8'
ctype = req.headers['Content-Type']
data = req.text
if ctype == "application/json":
data = json.loads(data)
elif ctype.startswith("text/html"):
data = parse_website(data)
else:
raise ValueError("Webpage provided is invalid (modpack platform not supported?)")
else:
with open(FILEPATH + "pack.html", 'r', encoding='utf-8') as fi:
data = fi.read()
data = parse_website(data)
if data is None:
with open(FILEPATH + "api_119.json", 'r') as fi:
#with open(FILEPATH + "api_119_11441.json", 'r') as fi:
#with open(FILEPATH + "api_119_11441_mods.json", 'r') as fi:
data = json.load(fi)
return data
def build_pack_version_url(provider: str, pack: int, version: int):
#return 'file://./local/api_%u_%u.json' % (pack, version)
if not provider.startswith("api."):
provider = 'api.' + provider
return 'https://%s/public/modpack/%u/%u' % (provider, pack, version)
def download_pack_version(url: str):
with requests.get(url) as req:
req.raise_for_status()
if not req.encoding:
req.encoding = 'utf-8'
data = req.text
data = json.loads(data)
return data
def main():
print("Enter *full* website URL to download modpack from")
print("(URL should look like https://<website>/modpacks/<modpack name>)")
url = input('> ')
data = download_pack_meta(url)
smeta: model.CHModpackMeta = json_to_obj(data, model.CHModpackMeta) # type: ignore
#TODO: version picker UI
max_verion = max(smeta.versions, default=None, key=lambda x:x.id)
assert max_verion is not None
version_url = build_pack_version_url(smeta.provider, smeta.id, max_verion.id)
data = download_pack_version(version_url)
sver: model.CHVersionMetadata = json_to_obj(data, model.CHVersionMetadata) # type: ignore
print("Parsing...")
res = model.CFManifest()
#TODO: parse /id JSON too, not just /id/version
res.name = "Modpack Converter example modpack"
res.version = "1.0.0"
res.author = "Modpack Converter"
if smeta:
res.name = smeta.name
res.version = max_verion.name
#HACK: no author field, not sure what to put there
#FIXME: security issue, sanitize paths and stuff
for target in sver.targets:
if target.type == 'game':
assert target.name == 'minecraft'
assert not res.minecraft.version
res.minecraft.version = target.version
elif target.type == 'modloader':
ml = model.CFModLoader()
ml.primary = not res.minecraft.modLoaders
ml.id = "%s-%s" % (target.name, target.version) # best guess
res.minecraft.modLoaders.append(ml)
to_download: dict[str, str] = dict()
#TODO: do not hardcode paths
for file in sver.files:
if file.serveronly:
continue
if file.curseforge:
assert file.type == 'mod', "Only mods can be downloaded via CF (%s)" % pp_format(file, 0)
cfmod = model.CFMod(projectID=file.curseforge.project, fileID=file.curseforge.file, required= not file.optional) # type: ignore
res.files.append(cfmod)
else:
assert file.url, "File that can not be downloaded (%s)" % (pp_format(file, 0))
folder = '%s/%s' % ('out/overrides', file.path)
os.makedirs(folder, exist_ok=True)
path = '%s/%s' % (folder, file.name)
to_download[path] = file.url
print("Writing manifest...")
with open('out/manifest.json', 'w') as fo:
json.dump(res, fo, ensure_ascii=False, default=pydefault, indent=4)
print("TODO: download files")
if True:
with requests.Session() as sess:
i = 0
ni = len(to_download)
for target, url in to_download.items():
i += 1
with open(target, 'wb') as fo:
with sess.get(url, stream=True) as req:
req.raise_for_status()
clen = 0
try:
clen_str = req.headers['Content-Length']
clen = int(clen_str, 10)
except:
pass
with ProgressBar(message="Downloading %4u/%4u" % (i, ni), suffix='[%(elapsed_td)s / %(eta_td)s] %(max)d <-- %(index)d', max=clen) as progress:
for chunk in req.iter_content(None):
fo.write(chunk)
progress.next(len(chunk))
else:
#pp(to_download, 4)
pass
print("Compressing...")
with zipfile.ZipFile('out/out.zip', 'w', allowZip64=False) as zip:
outfiles = glob.iglob('out/**', recursive=True)
for file in outfiles:
p = Path(file)
print(p)
if file.endswith('out.zip'):
continue
if not p.is_file():
continue
fn = file[4:]
with zip.open(fn, 'w') as fo:
with open(file, 'rb') as fi:
while True:
buf = fi.read(1024)
if not buf:
break
fo.write(buf)
if __name__ == '__main__':
main()