Доработка взаимодействия с API, новая процедура установки, определение установленных версий игры и начальная реализация БД

This commit is contained in:
Евгений Титаренко 2023-07-21 18:05:44 +03:00
parent 983d0f9e21
commit 5beb835d1e
4 changed files with 217 additions and 51 deletions

26
api.py
View file

@ -14,21 +14,22 @@ def download(file_url: str, file_size: int, path: str):
file.write(data) 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" api_url = "https://api.modrinth.com"
def request(**args): def request(**args):
sub_method = "" slug = ""
if "project" in args: if "project" in args:
sub_method = "/" + args.pop("project") slug = "/" + args.pop("project")
elif "version" in args: elif "version" in args:
sub_method = "/" + args.pop("version") slug = "/" + args.pop("version")
print(f"{api_url}/{api_version}{method}{sub_method}") # print(f"{api_url}/{api_version}{method}{slug}{sub_method}")
resp = requests.get(f"{api_url}/{api_version}{method}{sub_method}", params=args, headers=HEADERS) #print(args)
print(resp.headers.get("X-Ratelimit-Remaining", -1)) 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: match resp.status_code:
case 200: case 200:
print("200: OK") # print("200: OK")
if type(resp.json()) == list: if type(resp.json()) == list:
return [type(method, (object,), obj) for obj in resp.json()] return [type(method, (object,), obj) for obj in resp.json()]
return type(method, (object,), resp.json()) return type(method, (object,), resp.json())
@ -40,10 +41,10 @@ def __method(method: str, api_version: str = API_VERSION):
print("401: ERROR") print("401: ERROR")
case 404: case 404:
''' '''
The requested project was not found or The requested project was not found or no authorization to see this project
no authorization to see this project
''' '''
print("404: ERROR") # print("404: ERROR")
return None
resp.raise_for_status() resp.raise_for_status()
return request return request
@ -51,5 +52,6 @@ def __method(method: str, api_version: str = API_VERSION):
test = __method("", "") test = __method("", "")
search = __method("/search") search = __method("/search")
project = __method("/project") project = __method("/project")
check_project = __method("/project", sub_method="/check")
version = __method("/version") version = __method("/version")
versions = __method("/versions") get_versions = __method("/project", sub_method="/version")

130
mc-get.py
View file

@ -2,31 +2,16 @@
import argparse import argparse
import api import api
import mcfs import mcfs
import npyscreen
import os import os
import mcgetdb
from colorama import Fore, Style from colorama import Fore, Style
db = mcgetdb.McGetDB(mcfs.mc_dir)
def validate(): def validate():
raise NotImplementedError 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): def download_cache(url: str, filename: str, size: int):
cache_file_path = os.path.join(mcfs.cache_dir, filename) cache_file_path = os.path.join(mcfs.cache_dir, filename)
if not mcfs.is_path_exist(mcfs.cache_dir): if not mcfs.is_path_exist(mcfs.cache_dir):
@ -51,28 +36,72 @@ def modpack_install(filename: str):
mcfs.install_modpacks_override(filename) mcfs.install_modpacks_override(filename)
def install(projects: list): def install(projects: list, version, loader):
to_install = [] 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: for project in projects:
check_project(project)
for project in projects_ids:
project_data = api.project(project=project) project_data = api.project(project=project)
to_install.append(project_data) versions = api.get_versions(project=project, loaders=f'["{loader}"]',
for project in to_install: game_versions=f'["{mc_ver}"]')
versions = api.versions(ids=str(project.versions).replace("'", '"')) # I hate this if versions:
version = npyscreen.wrapper_basic(version_selector_gui(versions, to_install.append((project_data, versions[0]))
project.slug)) 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]) file = type("mc_file", (object,), version.files[0])
filename = file.url.split("/")[-1] filename = project.slug + ".jar"
download_cache(file.url, filename, file.size) 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: match project.project_type:
case "resourcepack": case "resourcepack":
subdir = "resourcepacks" subdir = "resourcepacks"
@ -114,6 +143,39 @@ def clean():
if __name__ == "__main__": 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" desc = "Minecraft mods packet manager based on Modrinth API"
parser = argparse.ArgumentParser(description=desc, parser = argparse.ArgumentParser(description=desc,
formatter_class=argparse.RawTextHelpFormatter) 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 = subparsers.add_parser("install", help="Install one or more mods or resources")
parser_install.add_argument("projects", nargs="+") 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 = subparsers.add_parser("search", help="Find a mod or a resource")
parser_search.add_argument("query", nargs="+") parser_search.add_argument("query", nargs="+")

59
mcfs.py
View file

@ -5,6 +5,23 @@ import zipfile
import json 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(): def __get_mc_dir():
directory = "" directory = ""
if platform == 'linux': if platform == 'linux':
@ -35,6 +52,14 @@ def __get_cache_dir():
return directory 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): def get_modpack_info(filename: str):
with zipfile.ZipFile(os.path.join(cache_dir, filename)) as modpack: with zipfile.ZipFile(os.path.join(cache_dir, filename)) as modpack:
with modpack.open("modrinth.index.json") as index: 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)) 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): def install(filename, subdir: str):
_from = os.path.join(cache_dir, filename) _from = os.path.join(cache_dir, filename)
_to = os.path.join(mc_dir, subdir, filename) _to = os.path.join(mc_dir, subdir, filename)
@ -69,5 +90,31 @@ def install(filename, subdir: str):
shutil.copy2(_from, _to) shutil.copy2(_from, _to)
mc_dir = __get_mc_dir() def get_installed_mc_versions():
cache_dir = __get_cache_dir() 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)

53
mcgetdb.py Normal file
View file

@ -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