|
| 1 | +import webbrowser |
| 2 | +import traceback |
| 3 | +import json |
| 4 | +import urllib.request |
| 5 | +import logging |
| 6 | +import os |
| 7 | +import hashlib |
| 8 | +import semantic_version |
| 9 | +import kilt.errors |
| 10 | + |
| 11 | +class versionData: |
| 12 | + site = "https://api.modrinth.com/api/v1/mod?query={}&limit={}&index={}&offset={}" |
| 13 | + __version__ = semantic_version.Version("0.1.0-alpha0+api.1") |
| 14 | + __major__ = __version__.major |
| 15 | + __minor__ = __version__.minor |
| 16 | + __patch__ = __version__.patch |
| 17 | + __prerelease__ = __version__.prerelease |
| 18 | + __build__ = __version__.build |
| 19 | + |
| 20 | + |
| 21 | +def alpha(): |
| 22 | + if versionData.__prerelease__[0] == "alpha": |
| 23 | + return True |
| 24 | + else: |
| 25 | + return False |
| 26 | + |
| 27 | + |
| 28 | +def beta(): |
| 29 | + if versionData.__prerelease__[0] == "beta": |
| 30 | + return True |
| 31 | + else: |
| 32 | + return False |
| 33 | + |
| 34 | + |
| 35 | +def api(): |
| 36 | + return versionData.__build__[1] |
| 37 | + |
| 38 | + |
| 39 | +def release(): |
| 40 | + if versionData.__prerelease__[0] == "": |
| 41 | + return True |
| 42 | + else: |
| 43 | + return False |
| 44 | + |
| 45 | + |
| 46 | +is_source = is_src = alpha |
| 47 | +is_github_release = beta |
| 48 | +is_pypi_release = release |
| 49 | + |
| 50 | + |
| 51 | + |
| 52 | +class ReturnValues: |
| 53 | + mod_exists_already = "mod_exists_already" |
| 54 | + |
| 55 | + # sets up logging |
| 56 | + if not os.path.exists("logs/"): |
| 57 | + os.mkdir("logs") |
| 58 | + logging_format = '[%(filename)s][%(funcName)s/%(levelname)s] %(message)s' |
| 59 | + logging.basicConfig(level=logging.DEBUG, filename="logs/debug.log", filemode="w", format=logging_format) |
| 60 | + |
| 61 | + |
| 62 | +def removekey(d, key): |
| 63 | + r = dict(d) |
| 64 | + del r[key] |
| 65 | + return r |
| 66 | + |
| 67 | + |
| 68 | +def get_version(version_type="full"): |
| 69 | + if version_type == "major": |
| 70 | + return versionData.__major__ |
| 71 | + elif version_type == "minor": |
| 72 | + return versionData.__minor__ |
| 73 | + elif version_type == "patch": |
| 74 | + return versionData.__patch__ |
| 75 | + elif version_type == "prerelease": |
| 76 | + return versionData.__prerelease__ |
| 77 | + elif version_type == "build": |
| 78 | + return versionData.__build__ |
| 79 | + elif version_type == "full": |
| 80 | + return versionData.__version__ |
| 81 | + |
| 82 | + |
| 83 | +def get_number_of_mods(): |
| 84 | + number_of_mods = 0 |
| 85 | + there_are_more_mods = True |
| 86 | + i = 0 |
| 87 | + while there_are_more_mods: |
| 88 | + mod_list = json.loads(urllib.request.urlopen(versionData.site.format("", 100, "newest", i * 100 + 1)).read())[ |
| 89 | + "hits"] |
| 90 | + number_of_mods += len(mod_list) |
| 91 | + if len(mod_list) == 0: |
| 92 | + there_are_more_mods = False |
| 93 | + i += 1 |
| 94 | + logging.info("There are {} mods on modrinth".format(number_of_mods)) |
| 95 | + return number_of_mods |
| 96 | + |
| 97 | + |
| 98 | +def search(id=None, id_array=[], get=True, saveIcon=False, logging_level=logging.INFO, crash=False, modlist=None, filemode="w", openweb=False, output=True, outputfile=None, index="relevance", |
| 99 | + offset=0, |
| 100 | + limit=10, saveDescriptionToFile=None, web_save=None, download_folder=None, body=False, search_array=[], |
| 101 | + repeat=1, search=""): |
| 102 | + # make sure arguments are correct |
| 103 | + failed_mods = [] |
| 104 | + valueToReturn = None |
| 105 | + extra_values = [] |
| 106 | + dict_of_pages = { |
| 107 | + "issues": 'issues_url', |
| 108 | + "source": 'source_url', |
| 109 | + "wiki": 'wiki_url', |
| 110 | + "discord": "discord_url", |
| 111 | + "donation": "donation_urls" |
| 112 | + } |
| 113 | + logger = logging.getLogger() |
| 114 | + logger.setLevel(logging_level) |
| 115 | + if type(saveIcon) is not bool: |
| 116 | + raise kilt.errors.InvalidArgument("{} (saveIcon) is not a boolean".format(saveIcon)) |
| 117 | + if type(limit) is not int or limit not in list(range(0, 101)): |
| 118 | + raise kilt.errors.InvalidArgument("{} (limit) is not in range 0, 100, or is not an integer.".format(limit)) |
| 119 | + if web_save not in list(dict_of_pages.keys()) and web_save not in {"home", None}: |
| 120 | + raise kilt.errors.InvalidArgument( |
| 121 | + "{} (web_save) is not in {}".format(web_save, (list(dict_of_pages.keys()), "home"))) |
| 122 | + if type(crash) is not bool: |
| 123 | + raise kilt.errors.InvalidArgument("{} (crash) is not a boolean".format(crash)) |
| 124 | + if filemode not in {"w", "a"}: |
| 125 | + raise kilt.errors.InvalidArgument("{} (filemode) is not in 'a' or 'w'".format(filemode)) |
| 126 | + if type(openweb) is not bool: |
| 127 | + raise kilt.errors.InvalidArgument("{} (openweb) is not a boolean".format(openweb)) |
| 128 | + if type(output) is not bool: |
| 129 | + raise kilt.errors.InvalidArgument("{} (output) is not a boolean".format(output)) |
| 130 | + if type(offset) is not int or offset not in list(range(0, 101)): |
| 131 | + raise kilt.errors.InvalidArgument("{} (offset) is not in range 0, 100, or it is not an integer".format(offset)) |
| 132 | + if type(body) is not bool: |
| 133 | + raise kilt.errors.InvalidArgument("{} (body) is not a boolean".format(body)) |
| 134 | + if type(repeat) is not int or repeat <= 0: |
| 135 | + raise kilt.errors.InvalidArgument("{} (repeat) is not an integer, or it is below 0.".format(repeat)) |
| 136 | + ### |
| 137 | + # searching of mods |
| 138 | + ## |
| 139 | + if not search_array: |
| 140 | + if search == "": |
| 141 | + index = "newest" |
| 142 | + search_array.append(search) |
| 143 | + patched_searches = [] |
| 144 | + for i in search_array: |
| 145 | + patched_searches.append(i.replace(" ","%20")) |
| 146 | + search_array = patched_searches |
| 147 | + logging.info("Mods to search for: {}".format(" ,".join(search_array))) |
| 148 | + if saveDescriptionToFile is not None: |
| 149 | + with open(saveDescriptionToFile, "w") as file: |
| 150 | + file.write("Mod Descriptions\n") |
| 151 | + if modlist is not None: |
| 152 | + with open(modlist, "w") as file: |
| 153 | + file.write("""<!DOCTYPE html> |
| 154 | + <html> |
| 155 | + <head> |
| 156 | + <title>Modlist</title> |
| 157 | + </head> |
| 158 | +
|
| 159 | + <body>""") |
| 160 | + for this_fake_var in range(repeat): |
| 161 | + for this_search in search_array: |
| 162 | + if id is None: |
| 163 | + modSearch = versionData.site.format(this_search, limit, index, offset) |
| 164 | + modSearchJson = json.loads(urllib.request.urlopen(modSearch).read()) |
| 165 | + try: |
| 166 | + mod_response = modSearchJson["hits"][0] |
| 167 | + except IndexError: |
| 168 | + if offset == 0 and repeat == 1: |
| 169 | + logging.info("There were no results for your search") |
| 170 | + raise kilt.errors.EndOfSearch("No results found for your query") |
| 171 | + elif offset == 0 and repeat != 1: |
| 172 | + logging.info("You hit the end of your search!") |
| 173 | + raise kilt.errors.EndOfSearch("You attempted to access search result {} but {} was the max".format(offset+1, offset)) |
| 174 | + else: |
| 175 | + break |
| 176 | + modJsonURL = "https://api.modrinth.com/api/v1/mod/" + str(mod_response["mod_id"].replace("local-", "")) |
| 177 | + else: |
| 178 | + modJsonURL = "https://api.modrinth.com/api/v1/mod/" + id |
| 179 | + mod_struct = json.loads(urllib.request.urlopen(modJsonURL).read()) |
| 180 | + if not get: |
| 181 | + extra_values = mod_struct |
| 182 | + if get: |
| 183 | + try: |
| 184 | + extra_values.append({"title": mod_struct["title"], "url": "https://modrinth.com/mod/{}".format(mod_struct["slug"]), |
| 185 | + "desc": mod_struct["description"], "id": mod_struct["id"]}) |
| 186 | + except AttributeError: |
| 187 | + pass |
| 188 | + if saveIcon: |
| 189 | + os.makedirs("cache", exist_ok=True) |
| 190 | + if not os.path.exists("cache/{}.png".format(mod_struct["title"])): |
| 191 | + if mod_struct["icon_url"] is None: |
| 192 | + logging.debug("{} does not have a mod icon".format(mod_struct["title"])) |
| 193 | + mod_icon_fileLikeObject = urllib.request.urlopen("https://raw.githubusercontent.com/Jefaxe" |
| 194 | + "/Kilt/main/meta/missing.png") |
| 195 | + else: |
| 196 | + mod_icon_fileLikeObject = urllib.request.urlopen(str(mod_struct["icon_url"])) |
| 197 | + with open("cache/{}.png".format(mod_struct["title"]), "wb") as file: |
| 198 | + file.write(mod_icon_fileLikeObject.read()) |
| 199 | + mod_struct_minus_body = removekey(mod_struct, "body") |
| 200 | + logging.debug( |
| 201 | + "[Labrinth] Requested mod json(minus body): {json}".format(json=mod_struct_minus_body)) |
| 202 | + # logging.debug("[Modrinth]: {json}".format(json=modSearchJson)) |
| 203 | + try: |
| 204 | + if offset >= modSearchJson['total_hits']: |
| 205 | + logging.error( |
| 206 | + "There are not THAT many in the search, set `limit` higher. Or that may be it all. NOTE THAT `offset` WILL BE SET TO 0") |
| 207 | + offset = 0 |
| 208 | + except UnboundLocalError: #when using 'id', modSearchJson is not defined |
| 209 | + pass |
| 210 | + # output events |
| 211 | + if saveDescriptionToFile is not None: |
| 212 | + with open(saveDescriptionToFile, "a") as desc: |
| 213 | + desc.write(mod_struct["title"] + ": " + mod_struct["description"] + "\n") |
| 214 | + # web events |
| 215 | + if web_save is not None: |
| 216 | + page = mod_response["page_url"] if web_save == "home" else mod_struct[dict_of_pages[web_save]] |
| 217 | + if openweb and page not in [None, [], ""]: |
| 218 | + webbrowser.open(page) |
| 219 | + logging.debug("[Knosses] Opened {}'s {} page at {}".format(mod_struct["title"], web_save, page)) |
| 220 | + valueToReturn = page |
| 221 | + try: |
| 222 | + downloadLink = \ |
| 223 | + json.loads(urllib.request.urlopen("{json}/version".format(json=modJsonURL)).read())[0]["files"][0][ |
| 224 | + "url"].replace(" ", "%20") |
| 225 | + except IndexError: |
| 226 | + logging.error("mod {} does not have any versions, skipping...".format(mod_response["title"])) |
| 227 | + downloadLink="https://modrinth.com/download-was-not-found-by-kilt" |
| 228 | + failed_mods.append(mod_response["title"]) |
| 229 | + valueToReturn = failed_mods |
| 230 | + # downloads |
| 231 | + if download_folder is not None: |
| 232 | + if type(download_folder) == bool and download_folder: |
| 233 | + download_folder = "mods" |
| 234 | + try: |
| 235 | + os.makedirs(download_folder, exist_ok=True) |
| 236 | + try: |
| 237 | + filename = \ |
| 238 | + json.loads(urllib.request.urlopen("{json}/version".format(json=modJsonURL)).read())[0][ |
| 239 | + "files"][0][ |
| 240 | + "filename"] |
| 241 | + except IndexError: |
| 242 | + logging.error("mod {} has no versions".format(mod_response["title"])) |
| 243 | + try: |
| 244 | + if filename in os.listdir(download_folder): |
| 245 | + logging.info( |
| 246 | + "[Kilt]{} is already downloaded (note we have only checked the filename, not the SHA1 hash".format( |
| 247 | + filename)) |
| 248 | + if crash: |
| 249 | + raise errors.AlreadyDownloaded("{}".format(filename)) |
| 250 | + else: |
| 251 | + valueToReturn = ReturnValues.mod_exists_already |
| 252 | + except UnboundLocalError: |
| 253 | + pass |
| 254 | + else: |
| 255 | + logging.info( |
| 256 | + "[Kilt] Downloading {mod} from {url}".format(mod=mod_struct["title"], url=downloadLink)) |
| 257 | + downloadUrrlib = urllib.request.urlopen(downloadLink) |
| 258 | + with open(download_folder + "/{mod}".format(mod=downloadLink.rsplit("/", 1)[-1]).replace("%20", |
| 259 | + " "), |
| 260 | + "wb") as modsave: |
| 261 | + modsave.write(downloadUrrlib.read()) |
| 262 | + BLOCK_SIZE = 65536 # The size of each read from the file |
| 263 | + file_hash = hashlib.sha256() # Create the hash object, can use something other than `.sha256()` if you wish |
| 264 | + with open(download_folder + "/{mod}".format(mod=downloadLink.rsplit("/", 1)[-1]).replace("%20", |
| 265 | + " "), |
| 266 | + 'rb') as f: # Open the file to read it's bytes |
| 267 | + fb = f.read(BLOCK_SIZE) # Read from the file. Take in the amount declared above |
| 268 | + while len(fb) > 0: # While there is still data being read from the file |
| 269 | + file_hash.update(fb) # Update the hash |
| 270 | + fb = f.read(BLOCK_SIZE) # Read the next block from the file |
| 271 | + valueToReturn = file_hash.hexdigest() # Get the hexadecimal digest of the hash |
| 272 | + except urllib.error.HTTPError as e: |
| 273 | + logging.critical( |
| 274 | + "[Labrinth] COULD NOT DOWNLOAD MOD {} from {} because {}".format(mod_response["title"], |
| 275 | + downloadLink, e)) |
| 276 | + failed_mods.append(mod_response["title"]) |
| 277 | + valueToReturn = failed_mods |
| 278 | + if modlist is not None: |
| 279 | + with open(modlist, "a") as file: |
| 280 | + file.write( |
| 281 | + "<image src={} width=64 height=64 alt={}><a href={}>{} (by {}): </a><a href={}>Download<p></p>".format( |
| 282 | + mod_struct["icon_url"], |
| 283 | + mod_struct["title"], |
| 284 | + mod_response["page_url"], |
| 285 | + mod_struct["title"], |
| 286 | + mod_response["author"], |
| 287 | + downloadLink)) |
| 288 | + if body: |
| 289 | + with open("generated/meta/{mod}".format(mod=mod_struct["title"] + ".md"), "w") as modsave: |
| 290 | + modsave.write(mod_struct["body"]) |
| 291 | + valueToReturn = mod_struct["body"] |
| 292 | + if not output: |
| 293 | + print(valueToReturn) |
| 294 | + if outputfile is not None: |
| 295 | + with open(outputfile, "w") as file: |
| 296 | + for x in [valueToReturn]: |
| 297 | + json.dump(x, file, indent=4) |
| 298 | + offset += 1 |
| 299 | + if modlist is not None: |
| 300 | + with open(modlist, "a") as file: |
| 301 | + file.write(""" </body> |
| 302 | +</html>""") |
| 303 | + #logging.info(extra_values) |
| 304 | + return [valueToReturn, extra_values] |
| 305 | + |
| 306 | + |
| 307 | +if __name__ == "__main__": |
| 308 | + try: |
| 309 | + search() |
| 310 | + except (Exception, SystemExit) as e: |
| 311 | + if type(e) == SystemExit: |
| 312 | + print(e) |
| 313 | + else: |
| 314 | + logging.error(traceback.format_exc()) |
| 315 | + print(e) |
| 316 | + print("The process ran into an error. The error can be found in version_data.log") |
0 commit comments