import json import logging import os import re import sys import traceback import zipfile import PySide6.QtCore as QtC import PySide6.QtGui as QtG import PySide6.QtWidgets as QtW import toml from slugify import slugify from solderPacker import SolderPacker # Globals config = {} base_path = os.path.dirname(__file__) class Form(QtW.QMainWindow): def __init__(self): QtW.QMainWindow.__init__(self) self.setMinimumSize(400, 500) self.setWindowIcon(QtG.QIcon(os.path.join(base_path, "wrenchIcon.ico"))) self.setWindowTitle("Solder Packer") self.thread_pool = QtC.QThreadPool() self.solder_packer = SolderPacker(config["path"], config["url"], config["token"]) self.jar_path = "" menubar = self.menuBar() options_action = menubar.addAction("Options") options_action.triggered.connect(self.open_options) main_widget = QtW.QWidget() layout = QtW.QGridLayout() mod_title_label = QtW.QLabel("Mod Name") self.mod_title_edit = QtW.QLineEdit("") layout.addWidget(mod_title_label, 1, 1) layout.addWidget(self.mod_title_edit, 1, 2) mod_slug_label = QtW.QLabel("Mod Slug") self.mod_slug_edit = QtW.QLineEdit("") layout.addWidget(mod_slug_label, 2, 1) layout.addWidget(self.mod_slug_edit, 2, 2) mod_version_label = QtW.QLabel("Mod Version") self.mod_version_edit = QtW.QLineEdit("") layout.addWidget(mod_version_label, 3, 1) layout.addWidget(self.mod_version_edit, 3, 2) mod_mc_version_label = QtW.QLabel("MC Version") self.mod_mc_version_edit = QtW.QLineEdit("") layout.addWidget(mod_mc_version_label, 4, 1) layout.addWidget(self.mod_mc_version_edit, 4, 2) mod_loader_label = QtW.QLabel("Mod Loader") self.mod_loader_edit = QtW.QLineEdit("") layout.addWidget(mod_loader_label, 5, 1) layout.addWidget(self.mod_loader_edit, 5, 2) mod_author_label = QtW.QLabel("Mod Author") self.mod_author_edit = QtW.QLineEdit("") layout.addWidget(mod_author_label, 6, 1) layout.addWidget(self.mod_author_edit, 6, 2) mod_url_label = QtW.QLabel("Mod URL") self.mod_url_edit = QtW.QLineEdit("") layout.addWidget(mod_url_label, 7, 1) layout.addWidget(self.mod_url_edit, 7, 2) mod_desc_label = QtW.QLabel("Mod Description") mod_desc_label.setAlignment(QtC.Qt.AlignmentFlag.AlignCenter) self.mod_desc_edit = QtW.QPlainTextEdit("") self.mod_desc_edit.setMinimumSize(200, 50) layout.addWidget(mod_desc_label, 8, 1, 1, 2) layout.addWidget(self.mod_desc_edit, 9, 1, 1, 2) horizontal_line = QtW.QFrame() horizontal_line.setFrameShape(QtW.QFrame.Shape.HLine) horizontal_line.setFrameShadow(QtW.QFrame.Shadow.Sunken) layout.addWidget(horizontal_line, 10, 1, 1, 2) self.drop_label = FileDropArea("Drop JAR Here", self) self.drop_label.setMinimumSize(200, 50) self.drop_label.setAlignment(QtC.Qt.AlignmentFlag.AlignCenter) self.drop_label.setFrameStyle(QtW.QFrame.Shadow.Sunken | QtW.QFrame.Shape.StyledPanel) self.drop_label.dropped.connect(self.file_dropped) layout.addWidget(self.drop_label, 11, 1, 1, 2) self.btn_process = QtW.QPushButton("Process") self.btn_process.clicked.connect(self.process_click) layout.addWidget(self.btn_process, 12, 1, 1, 2) self.setCentralWidget(main_widget) main_widget.setLayout(layout) def open_options(self): options_dialog = OptionsDialog(parent=self) options_dialog.closed.connect(self.options_closed) options_dialog.show() def options_closed(self): self.solder_packer.solder_dir = config["path"] self.solder_packer.solder_url = config["url"] self.solder_packer.solder_key = config["token"] def file_dropped(self, file_path: str): self.jar_path = file_path if config["extract"]: jar = zipfile.ZipFile(file_path, "r") if "META-INF/mods.toml" in jar.namelist(): # This jar file uses FML logging.debug("identified mod as forge mod") mod_toml = toml.loads(jar.read("META-INF/mods.toml").decode("utf-8")) # This mess is to catch if the mod is using the Jar version for versioning. It extracts it from the # MANIFEST.MF and places it where it should be in the toml. if mod_toml["mods"][0]["version"] == "${file.jarVersion}": mod_toml["mods"][0]["version"] = re.findall( r"Implementation-Version: (.*)", jar.read("META-INF/MANIFEST.MF").decode("utf-8"))[0] # Update form with data self.mod_title_edit.setText(mod_toml.get("mods", [{}])[0].get("displayName", "")) self.mod_slug_edit.setText(slugify(mod_toml.get("mods", [{}])[0].get("displayName", ""), replacements=[["&", "and"], ["$", "s"], ["'", ""]])) self.mod_version_edit.setText(mod_toml.get("mods", [{}])[0].get("version", "")) self.mod_loader_edit.setText("forge") self.mod_author_edit.setText(mod_toml.get("mods", [{}])[0].get("authors", "")) self.mod_url_edit.setText(mod_toml.get("mods", [{}])[0].get("displayURL", "")) self.mod_desc_edit.setPlainText(mod_toml.get("mods", [{}])[0].get("description", "")) elif "fabric.mod.json" in jar.namelist(): # This jar file uses Fabric logging.debug("identified mod as fabric mod") mod_json = json.loads(jar.read("fabric.mod.json").decode("utf-8")) # Update form with data self.mod_title_edit.setText(mod_json.get("name", "")) self.mod_slug_edit.setText(slugify(mod_json.get("name", ""), replacements=[["&", "and"], ["$", "s"], ["'", ""]])) self.mod_version_edit.setText(mod_json.get("version", "")) self.mod_loader_edit.setText("fabric") # Some spaghetti code for dealing with the possibility of authors being a []string or a dict. authors = mod_json.get("authors", []) author_list = [] if len(authors) > 0 and type(authors[0]) == dict: for author in authors: author_list.append(author.get("name")) elif len(authors) > 0 and type(authors[0]) == str: author_list = authors self.mod_author_edit.setText(", ".join(author_list)) self.mod_url_edit.setText(mod_json.get("contact", {}).get("homepage", "")) self.mod_desc_edit.setPlainText(mod_json.get("description", "")) elif "quilt.mod.json" in jar.namelist(): # This jar file uses Quilt logging.debug("identified mod as quilt mod") mod_json = json.loads(jar.read("quilt.mod.json").decode("utf-8")) # Update form with data self.mod_title_edit.setText(mod_json.get("quilt_loader", {}).get("metadata", {}).get("name", "")) self.mod_slug_edit.setText(slugify(mod_json.get("quilt_loader", {}).get("metadata", {}).get("name", ""), replacements=[["&", "and"], ["$", "s"], ["'", ""]])) self.mod_version_edit.setText(mod_json.get("quilt_loader", {}).get("version", "")) self.mod_loader_edit.setText("quilt") self.mod_author_edit.setText( ", ".join(mod_json.get("quilt_loader", {}).get("metadata", {}).get("contributors", {}).keys())) self.mod_url_edit.setText( mod_json.get("quilt_loader", {}).get("metadata", {}).get("contact", {}).get("homepage", "")) self.mod_desc_edit.setPlainText( mod_json.get("quilt_loader", {}).get("metadata", {}).get("description", "")) else: logging.debug("failed to identify mod loader") jar.close() def process_click(self): self.btn_process.setEnabled(False) self.btn_process.setText("Processing...") worker = Worker(self.process, self.mod_slug_edit.text(), self.mod_title_edit.text(), self.mod_version_edit.text(), self.mod_mc_version_edit.text(), self.mod_loader_edit.text(), self.mod_author_edit.text(), self.mod_url_edit.text(), self.mod_desc_edit.toPlainText(), self.jar_path) worker.signals.finished.connect(self.finished_processing) worker.signals.error.connect(self.failed_processing) self.thread_pool.start(worker) def process(self, mod_slug: str, mod_title: str, mod_version: str, mod_mc_version: str, mod_loader: str, mod_author: str, mod_url: str, mod_desc: str, jar_path: str): logging.debug("attempting jar compression") self.solder_packer.compress_jar(mod_slug, mod_mc_version, mod_loader, mod_version, jar_path) logging.debug("jar compression done") if config["upload"]: logging.debug("attempting to register mod with solder") self.solder_packer.register_mod(mod_slug, mod_title, mod_author, mod_desc, mod_url) logging.debug("registration complete") logging.debug("attempting to register mod version with solder") self.solder_packer.register_mod_version(mod_slug, mod_mc_version, mod_loader, mod_version) logging.debug("registration complete") def finished_processing(self): self.btn_process.setEnabled(True) self.btn_process.setText("Process") def failed_processing(self): dialog = QtW.QMessageBox(self) dialog.setWindowTitle("Error") dialog.setText("There was an error processing this JAR.") dialog.exec() self.finished_processing() class FileDropArea(QtW.QLabel): dropped = QtC.Signal(str) def __init__(self, title, parent): super(FileDropArea, self).__init__(title, parent) self.parent = parent self.setAcceptDrops(True) def dragEnterEvent(self, e): e.accept() @QtC.Slot() def dropEvent(self, e): file_path = e.mimeData().urls()[0].toLocalFile() self.dropped.emit(file_path) self.setText(os.path.basename(file_path)) class OptionsDialog(QtW.QDialog): closed = QtC.Signal() def __init__(self, parent=None): QtW.QDialog.__init__(self, parent) self.setMinimumSize(300, 200) self.setWindowIcon(QtG.QIcon(os.path.join(base_path, "wrenchIcon.ico"))) self.setWindowTitle("Options") layout = QtW.QGridLayout() self.setLayout(layout) layout.addWidget(QtW.QLabel("Export Folder"), 1, 1) layout.addWidget(QtW.QLabel("Technic URL"), 2, 1) layout.addWidget(QtW.QLabel("Technic Cookie"), 3, 1) self.input_export_folder = QtW.QLineEdit(config["path"]) layout.addWidget(self.input_export_folder, 1, 2) self.input_technic_url = QtW.QLineEdit(config["url"]) layout.addWidget(self.input_technic_url, 2, 2) self.input_technic_cookie = QtW.QLineEdit(config["token"]) layout.addWidget(self.input_technic_cookie, 3, 2) self.checkbox_extract_data = QtW.QCheckBox("Extract Mod Info") self.checkbox_extract_data.setChecked(config["extract"]) layout.addWidget(self.checkbox_extract_data, 4, 1) self.checkbox_upload_data = QtW.QCheckBox("Upload Data") self.checkbox_upload_data.setChecked(config["upload"]) layout.addWidget(self.checkbox_upload_data, 4, 2) button_save = QtW.QPushButton("Save") layout.addWidget(button_save, 5, 1, 1, 2) button_save.clicked.connect(self.save_options) def save_options(self): config["path"] = self.input_export_folder.text() config["url"] = self.input_technic_url.text() config["token"] = self.input_technic_cookie.text() config["extract"] = (self.checkbox_extract_data.checkState() == QtC.Qt.CheckState.Checked) config["upload"] = (self.checkbox_upload_data.checkState() == QtC.Qt.CheckState.Checked) with open(f"{base_path}{os.sep}config.toml", "w") as f: toml.dump(config, f) f.close() self.close() @QtC.Slot() def closeEvent(self, event): self.closed.emit() class WorkerSignals(QtC.QObject): finished = QtC.Signal() error = QtC.Signal(tuple) class Worker(QtC.QRunnable): def __init__(self, fn, *args, **kwargs): super().__init__() self.fn = fn self.args = args self.kwargs = kwargs self.signals = WorkerSignals() @QtC.Slot() def run(self): try: self.fn(*self.args, **self.kwargs) except: exec_type, value = sys.exc_info()[:2] self.signals.error.emit((exec_type, value, traceback.format_exc())) finally: self.signals.finished.emit() def get_default(struct: dict, key: str, default: any): return default if key not in struct else struct[key] if __name__ == '__main__': logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) try: config = toml.load(os.path.join(os.getcwd(), "config.toml")) except FileNotFoundError: config = {} config["path"] = get_default(config, "path", os.getcwd()) config["url"] = get_default(config, "url", os.environ.get("SOLDER_URL") or "http://localhost") config["token"] = get_default(config, "token", os.environ.get("SOLDER_KEY") or "") config["extract"] = get_default(config, "extract", True) config["upload"] = get_default(config, "upload", False) with open(os.path.join(os.getcwd(), "config.toml"), "w") as f: toml.dump(config, f) f.close() app = QtW.QApplication(sys.argv) form = Form() form.show() sys.exit(app.exec())