#!/usr/bin/env python3 import argparse import api import mcfs import os import mcgetdb from colorama import Fore, Style class HashError(ValueError): """Incorrect hash""" def validate(): raise NotImplementedError def download_cache(url: str, filename: str, size: int, hash: str): cache_file_path = os.path.join(mcfs.cache_dir, filename) if not mcfs.is_path_exist(mcfs.cache_dir): os.mkdir(mcfs.cache_dir) if not mcfs.is_path_exist(cache_file_path): api.download(url, size, cache_file_path) else: print(f"{filename} is in cache.") if not mcfs.check_file_hash(cache_file_path, hash): os.remove(cache_file_path) raise HashError(f"Incorrect hash for {filename}") def modpack_install(filename: str): modpack = mcfs.get_modpack_info(filename) print(f"{Fore.YELLOW}Installing {modpack.name} modpack...{Style.RESET_ALL}") for file in modpack.files: path = file["path"].split("/") downloads = file["downloads"] print(file["path"]) download_cache(downloads[0], path[1], file["fileSize"], file["hashes"]["sha512"]) mcfs.install(path[1], path[0]) print(f"{Fore.YELLOW}Overriding...{Style.RESET_ALL}") mcfs.install_modpacks_override(filename) def install(projects: list, mc_ver, loader): to_install = [] dependencies_to_install = [] not_found = [] unavailable = [] projects_ids = [] already_installed = [] def get_project_info_from_db(slug): _db = mcgetdb.McGetDB(mcfs.mc_dir) return _db.select_mod(slug) + _db.select_shader(slug) + _db.select_resourcepack(slug) + _db.select_modpack(slug) def check_project(slug): if __name__ == "__main__": mod_info = get_project_info_from_db(slug) if mod_info: already_installed.append(mod_info[0]) return id = api.check_project(project=slug) if id: projects_ids.append(id.id) else: not_found.append(slug) def dependency_solver(ver): for dependency in ver.dependencies: if dependency["dependency_type"] in ["optional", "embedded"]: continue if dependency["version_id"]: dep_ver = api.version(version=dependency["version_id"]) else: dep_ver = api.get_versions(project=dependency["project_id"], loaders=f'["{loader}"]', game_versions=f'["{mc_ver}"]')[0] if dependency["project_id"]: proj_id = dependency["project_id"] else: proj_id = dep_ver.project_id if proj_id in projects_ids or proj_id in [dep_proj.id for dep_proj, _ in dependencies_to_install]: continue project_data = api.project(project=proj_id) dependencies_to_install.append((project_data, dep_ver)) dependency_solver(dep_ver) for project in projects: check_project(project) for project in projects_ids: project_data = api.project(project=project) match project_data.project_type: case "resourcepack": versions = api.get_versions(project=project, game_versions=f'["{mc_ver}"]') case "mod" | "modpack": versions = api.get_versions(project=project, loaders=f'["{loader}"]', game_versions=f'["{mc_ver}"]') case "shader": # TODO: Реализовать поддержку загрузчиков шейдеров versions = api.get_versions(project=project, game_versions=f'["{mc_ver}"]') case _: raise NotImplementedError if versions: to_install.append((project_data, versions[0])) dependency_solver(versions[0]) else: unavailable.append(project_data) if to_install: print("To install:", *[project.title + " " + version.version_number for project, version in to_install], sep="\n\t") if dependencies_to_install: print("With dependencies:", *[project.title + " " + version.version_number for project, version in dependencies_to_install], sep="\n\t") if not_found: print("Not found: ", end="") print(*[project for project in not_found], sep=", ") if unavailable: print("Cannot be installed:", *[project.title for project in unavailable], sep="\n\t") if already_installed: print("Already installed:", *[name + " " + version for _, name, _, version, _ in already_installed], sep="\n\t") all_to_install = to_install + dependencies_to_install if not all_to_install: return choose = input("Continue? [y/n] ") if choose.strip().lower() in ["n", "no"]: print("Canceled.") return failed_to_install = [] for project, version in all_to_install: file = type("mc_file", (object,), version.files[0]) filename = file.filename hash = file.hashes["sha512"] try: download_cache(file.url, filename, file.size, hash) except HashError: print(f"Failed to install {project.title} ({project.slug}) [{version.version_number}] due to an incorrect " f"hash") failed_to_install.append((project, version)) continue match project.project_type: case "resourcepack": subdir = "resourcepacks" case "mod": subdir = "mods" case "shader": subdir = "shaderpacks" case "modpack": modpack_install(filename) subdir = "modpacks" case _: raise NotImplementedError if __name__ == "__main__": db = mcgetdb.McGetDB(mcfs.mc_dir) match project.project_type: case "resourcepack": db.add_resourcepack(slug=project.slug, proj_name=project.title, filename=filename, version=version.version_number, hash=hash) case "mod": db.add_mod(slug=project.slug, proj_name=project.title, filename=filename, version=version.version_number, hash=hash) case "shader": db.add_shader(slug=project.slug, proj_name=project.title, filename=filename, version=version.version_number, hash=hash) case "modpack": db.add_modpack(slug=project.slug, proj_name=project.title, filename=filename, version=version.version_number, hash=hash) case _: raise NotImplementedError mcfs.install(filename, subdir) if failed_to_install: print("Failed to install:", *[project.title + " " + version.version_number for project, version in failed_to_install], sep="\n\t") def search(query: list): results = api.search(query=' '.join(query)) for result in results.hits: print(Fore.GREEN, end="") print(result.get("slug", "error"), end="") print(Style.RESET_ALL, end="") print(f' [{result.get("project_type", "error")}] ', end="") print( f' : {Fore.GREEN}{result.get("title", "error")}{Style.RESET_ALL} --- {result.get("description", "error")}') def clean(): if mcfs.is_path_exist(mcfs.cache_dir): files = os.listdir(mcfs.cache_dir) if len(files) > 0: for file in files: os.remove(os.path.join(mcfs.cache_dir, file)) print("Cache cleared successfully.") return print("Nothing to clear.") def remove(projects): # TODO: 1. Проверка зависимостей? 2. Модпаки if __name__ == "__main__": db = mcgetdb.McGetDB(mcfs.mc_dir) mods_to_remove = [] resources_to_remove = [] shaders_to_remove = [] modpacks_to_remove = [] for project in projects: for res in db.select_mod(project): mods_to_remove.append(res) for res in db.select_resourcepack(project): resources_to_remove.append(res) for res in db.select_shader(project): shaders_to_remove.append(res) for res in db.select_modpack(project): modpacks_to_remove.append(res) for slug, title, filename, version, _ in mods_to_remove: local_path = os.path.join("mods", filename) mcfs.remove(local_path) db.remove_mod(slug) print(f"Mod {title} v.{version} was removed.") for _, title, filename, version, _ in resources_to_remove: local_path = os.path.join("resourcepacks", filename) mcfs.remove(local_path) db.remove_resourcepack(slug) print(f"Resource pack {title} v.{version} was removed.") for _, title, filename, version, _ in shaders_to_remove: local_path = os.path.join("shaderpacks", filename) mcfs.remove(local_path) db.remove_shader(slug) print(f"Shader pack {title} v.{version} was removed.") for _, title, filename, version, _ in modpacks_to_remove: raise NotImplementedError # local_path = os.path.join("modpacks", filename) # mcfs.remove(local_path) # db.remove_modpack(slug) # print(f"Mod pack {title} v.{version} was removed.") else: raise NotImplementedError("Not available in module mode") if __name__ == "__main__": def exit(): import sys sys.exit("MC installation not found. If the program is not installed in the default location, " "then specify the path to the installation through the MC_DIR environment variable.") if not mcfs.is_path_exist(mcfs.mc_dir): exit() db = mcgetdb.McGetDB(mcfs.mc_dir) def __select_version(versions): if not versions: return None print("Installed MC versions: ") for id, version in enumerate(versions): print(id + 1, version.version_number + (f" with {version.modloader}" if version.is_modified else ""), sep=": ") if len(versions) > 1: id_to_use = -1 while id_to_use not in range(1, len(versions)): id_input = input(f"Select MC version to use [1-{len(versions)}]: ") if not id_input.isnumeric(): continue id_to_use = int(id_input) else: id_to_use = 1 selected_version = versions[id_to_use - 1] print("Selected MC version:", selected_version.version_number + (f" with {selected_version.modloader}" if selected_version.is_modified else "")) return selected_version properties = db.get_properties() if not properties: version_to_use = __select_version(mcfs.get_installed_mc_versions()) if version_to_use: if version_to_use.is_modified: # TODO: Добавить иерархию каталогов db.set_properties(version_to_use.version_number, version_to_use.modloader) else: db.set_properties(version_to_use.version_number) properties = db.get_properties() else: properties = None if properties: _, mc_ver, loader, _ = properties else: mc_ver, loader = None, None desc = "Minecraft mods packet manager based on Modrinth API" parser = argparse.ArgumentParser(description=desc, formatter_class=argparse.RawTextHelpFormatter) subparsers = parser.add_subparsers(dest="method", required=True) # Переменная, в которую будет записано имя подкоманды parser_install = subparsers.add_parser("install", help="Install one or more mods or resources") parser_install.add_argument("projects", nargs="+") parser_install.add_argument("--mc_ver", default=mc_ver) parser_install.add_argument("--loader", default=loader) parser_search = subparsers.add_parser("search", help="Find a mod or a resource") parser_search.add_argument("query", nargs="+") parser_validate = subparsers.add_parser("validate", help="Validate the installation") parser_clean = subparsers.add_parser("clean", help="Clean the cache of this program") parser_remove = subparsers.add_parser("remove", help="Remove installed packages") parser_remove.add_argument("projects", nargs="+") kwargs = vars(parser.parse_args()) # Получаем все поля получившегося Namespace и пихаем в словарь if not properties: exit() globals()[kwargs.pop("method")](**kwargs) # Из глобального контекста получаем функцию с названием как в method, # заодно вытаскивая название метода из списка аргументов, # затем вызываем функцию с распакованным словарём в качестве аргумента