diff --git a/api.py b/api.py index 437bc30..d4aa031 100644 --- a/api.py +++ b/api.py @@ -14,21 +14,22 @@ def download(file_url: str, file_size: int, path: str): file.write(data) -def __method(method: str, api_version: str = API_VERSION): +def __method(method: str, sub_method = "", api_version: str = API_VERSION): api_url = "https://api.modrinth.com" def request(**args): - sub_method = "" + slug = "" if "project" in args: - sub_method = "/" + args.pop("project") + slug = "/" + args.pop("project") elif "version" in args: - sub_method = "/" + args.pop("version") - print(f"{api_url}/{api_version}{method}{sub_method}") - resp = requests.get(f"{api_url}/{api_version}{method}{sub_method}", params=args, headers=HEADERS) - print(resp.headers.get("X-Ratelimit-Remaining", -1)) + slug = "/" + args.pop("version") + # print(f"{api_url}/{api_version}{method}{slug}{sub_method}") + #print(args) + resp = requests.get(f"{api_url}/{api_version}{method}{slug}{sub_method}", params=args, headers=HEADERS) + # print(resp.headers.get("X-Ratelimit-Remaining", -1)) match resp.status_code: case 200: - print("200: OK") + # print("200: OK") if type(resp.json()) == list: return [type(method, (object,), obj) for obj in resp.json()] return type(method, (object,), resp.json()) @@ -40,10 +41,10 @@ def __method(method: str, api_version: str = API_VERSION): print("401: ERROR") case 404: ''' - The requested project was not found or - no authorization to see this project + The requested project was not found or no authorization to see this project ''' - print("404: ERROR") + # print("404: ERROR") + return None resp.raise_for_status() return request @@ -51,5 +52,6 @@ def __method(method: str, api_version: str = API_VERSION): test = __method("", "") search = __method("/search") project = __method("/project") +check_project = __method("/project", sub_method="/check") version = __method("/version") -versions = __method("/versions") +get_versions = __method("/project", sub_method="/version") diff --git a/mc-get.py b/mc-get.py index 4bf9685..86d46ab 100755 --- a/mc-get.py +++ b/mc-get.py @@ -2,31 +2,16 @@ import argparse import api import mcfs -import npyscreen import os +import mcgetdb from colorama import Fore, Style +db = mcgetdb.McGetDB(mcfs.mc_dir) def validate(): raise NotImplementedError -def version_selector_gui(vers: list, project: str): - def form(*args): - f = npyscreen.Form(name=f"Select {project} version") - sel = f.add(npyscreen.TitleSelectOne, value=[0, ], name="versions:", - values=[ver.version_number + " for " + - ", ".join(ver.game_versions) - for ver in vers[::-1]], scroll_exit=True) - f.edit() - for ver in vers: - if ver.version_number == sel.get_selected_objects()[0].split()[0]: - return ver - return vers[0] - - return form - - def download_cache(url: str, filename: str, size: int): cache_file_path = os.path.join(mcfs.cache_dir, filename) if not mcfs.is_path_exist(mcfs.cache_dir): @@ -51,28 +36,72 @@ def modpack_install(filename: str): mcfs.install_modpacks_override(filename) -def install(projects: list): +def install(projects: list, version, loader): to_install = [] + dependencies_to_install = [] + not_found = [] + unavailable = [] + projects_ids = [] + + def check_project(slug): + 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) - to_install.append(project_data) - for project in to_install: - versions = api.versions(ids=str(project.versions).replace("'", '"')) # I hate this - version = npyscreen.wrapper_basic(version_selector_gui(versions, - project.slug)) + versions = api.get_versions(project=project, loaders=f'["{loader}"]', + game_versions=f'["{mc_ver}"]') + if versions: + to_install.append((project_data, versions[0])) + dependency_solver(versions[0]) + else: + unavailable.append(project_data) + 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") + + choose = input("Continue? [y/n]") + if choose.strip().lower() in ["n", "no"]: + print("Canceled.") + return + + for project, version in to_install + dependencies_to_install: file = type("mc_file", (object,), version.files[0]) - filename = file.url.split("/")[-1] + filename = project.slug + ".jar" download_cache(file.url, filename, file.size) - # 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(file.url, file.size, cache_file_path) - # else: - # print(f"{filename} is in cache.") - - subdir = "" match project.project_type: case "resourcepack": subdir = "resourcepacks" @@ -114,6 +143,39 @@ def clean(): if __name__ == "__main__": + + def __select_version(versions): + if len(versions) > 0: + 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.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() + print(properties) + _, mc_ver, loader, _ = properties + desc = "Minecraft mods packet manager based on Modrinth API" parser = argparse.ArgumentParser(description=desc, formatter_class=argparse.RawTextHelpFormatter) @@ -122,6 +184,8 @@ if __name__ == "__main__": parser_install = subparsers.add_parser("install", help="Install one or more mods or resources") parser_install.add_argument("projects", nargs="+") + parser_install.add_argument("--version", 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="+") diff --git a/mcfs.py b/mcfs.py index ab746a1..c852838 100644 --- a/mcfs.py +++ b/mcfs.py @@ -5,6 +5,23 @@ import zipfile import json +class MCVersion: + def __init__(self, version_number, is_modified=False, modloader=None): + self.version_number = version_number + self.is_modified = is_modified + self.modloader = modloader + + def __lt__(self, other): + own_nums = map(int, self.version_number.split(".")) + other_nums = map(int, other.version_number.split(".")) + for own_num, other_num in zip(own_nums, other_nums): + if own_num < other_num: + return True + if own_num > other_num: + return False + return False + + def __get_mc_dir(): directory = "" if platform == 'linux': @@ -35,6 +52,14 @@ def __get_cache_dir(): return directory +def __version_sort(mc_ver: MCVersion) -> str: + return mc_ver.version_number + + +mc_dir = __get_mc_dir() +cache_dir = __get_cache_dir() + + def get_modpack_info(filename: str): with zipfile.ZipFile(os.path.join(cache_dir, filename)) as modpack: with modpack.open("modrinth.index.json") as index: @@ -58,10 +83,6 @@ def is_path_exist(path: str): return os.path.exists(os.path.join(path)) -def is_standard_dir_structure(): - return not os.path.exists(os.path.join(mc_dir, "home")) - - def install(filename, subdir: str): _from = os.path.join(cache_dir, filename) _to = os.path.join(mc_dir, subdir, filename) @@ -69,5 +90,31 @@ def install(filename, subdir: str): shutil.copy2(_from, _to) -mc_dir = __get_mc_dir() -cache_dir = __get_cache_dir() +def get_installed_mc_versions(): + mc_vers_dir = os.path.join(mc_dir, "versions") + if not is_path_exist(mc_vers_dir): + return # TODO: Выброс ошибки + versions_dirs = next(os.walk(mc_vers_dir))[1] + versions = [] + for version_dir in versions_dirs: + version_json_file = os.path.join(mc_vers_dir, version_dir, version_dir + ".json") + if not is_path_exist(version_json_file): + continue + with open(version_json_file) as json_file: + version_json = json.load(json_file) + mc_ver = None + match version_json.get("type", None): + case "modified": + version_number = version_json["jar"] # TODO: ИСПОЛЬЗОВАТЬ get ВМЕСТО [] + is_modified = True + modloader = version_json["id"].split()[0].lower() + mc_ver = MCVersion(version_number, is_modified, modloader) + case "release": # TODO: Добавить поддержку других значений + version_number = version_json["id"] + mc_ver = MCVersion(version_number) + case None: + pass + case _: # TODO: Throw some errors + pass + versions.append(mc_ver) + return sorted(versions, reverse=True) diff --git a/mcgetdb.py b/mcgetdb.py new file mode 100644 index 0000000..6d0bb38 --- /dev/null +++ b/mcgetdb.py @@ -0,0 +1,53 @@ +import sqlite3 +import os + +DB_VERSION = 1 + +class McGetDB: + + def __init_db(self): + with self.db as db: + db.execute(''' + CREATE TABLE IF NOT EXISTS mods ( + slug TEXT PRIMARY KEY NOT NULL, + install_path TEXT NOT NULL, + version TEXT NOT NULL + );''') + db.execute(''' + CREATE TABLE IF NOT EXISTS properties ( + db_version TEXT PRIMARY KEY NOT NULL, + version TEXT NOT NULL, + modloader TEXT, + dir_hierarchy TEXT + );''') + + def __init__(self, mc_path): + self.db_path = os.path.join(mc_path, "mcget.db") + self.db = sqlite3.connect(self.db_path) + self.__init_db() + + def get_properties(self): + properties = None + with self.db as db: + properties = db.execute(''' + SELECT * FROM properties; + ''').fetchone() + return properties + + def set_properties(self, mc_ver, modloader = "NULL", dir_hierarhy = "default"): + with self.db as db: + db.execute(''' + DELETE FROM properties; + ''') + db.execute(''' + INSERT INTO properties VALUES (?, ?, ?, ?) + ''', (DB_VERSION, mc_ver, modloader, dir_hierarhy)) + + def add_mod(self): + pass + + def remove_mod(self): + pass + + def update_mod(self): + pass