diff --git a/app/common/config.py b/app/common/config.py index b85370f..6620d4c 100644 --- a/app/common/config.py +++ b/app/common/config.py @@ -36,17 +36,17 @@ class Config(QConfig): """ Config of application """ # core - gamePath = ConfigItem("Core", "GamePath", "", FolderValidator()) + gamePath = ConfigItem("Core", "GamePath", "C:/Program Files (x86)/Steam/steamapps/common/Koikatsu Party", FolderValidator()) # createBackup backupEnable = ConfigItem( "CreateBackup", "Enable", False, BoolValidator() ) backupPath = ConfigItem( - "CreateBackup", "OutputPath", "", FolderValidator() + "CreateBackup", "OutputPath", "C:/Backup", FolderValidator() ) filename = ConfigItem( - "CreateBackup", "Filename", "", + "CreateBackup", "Filename", "koikatsu_backup", ) userData = ConfigItem( "CreateBackup", "UserData", False, BoolValidator() @@ -73,7 +73,7 @@ class Config(QConfig): installEnable = ConfigItem( "InstallChara", "Enable", False, BoolValidator() ) - gamePath = ConfigItem( + installPath = ConfigItem( "InstallChara", "InputPath", "", FolderValidator()) fileConflicts = OptionsConfigItem( "InstallChara", "FileConflicts", "Skip", OptionsValidator(["Skip", "Replace", "Rename"]) @@ -86,8 +86,8 @@ class Config(QConfig): removeEnable = ConfigItem( "RemoveChara", "Enable", False, BoolValidator() ) - gamePath = ConfigItem( - "InstallChara", "InputPath", "", FolderValidator()) + removePath = ConfigItem( + "RemoveChara", "InputPath", "", FolderValidator()) # main window micaEnabled = ConfigItem("MainWindow", "MicaEnabled", isWin11(), BoolValidator()) diff --git a/app/common/file_manager.py b/app/common/file_manager.py deleted file mode 100644 index 2286cb3..0000000 --- a/app/common/file_manager.py +++ /dev/null @@ -1,169 +0,0 @@ -import os -import shutil -import datetime -import patoolib -import customtkinter -import subprocess -import time - -from app.common.logger import logger - - -class FileManager: - - def __init__(self, config): - self.config = config - - def findAllFiles(self, directory): - fileList = [] - archiveList = [] - archiveExtensions = [".rar", ".zip", ".7z"] - - for root, dirs, files in os.walk(directory): - for file in files: - - filePath = os.path.join(root, file) - fileSize = os.path.getsize(filePath) - _, fileExtension = os.path.splitext(filePath) - - if fileExtension in archiveExtensions: - archiveList.append((filePath, fileSize, fileExtension)) - else: - fileList.append((filePath, fileSize, fileExtension)) - - fileList.sort(key=lambda x: x[1]) - archiveList.sort(key=lambda x: x[1]) - return fileList, archiveList - - def copyAndPaste(self, type, sourcePath, destinationFolder): - sourcePath = sourcePath[0] - baseName = os.path.basename(sourcePath) - destinationPath = os.path.join(destinationFolder, baseName) - conflicts = self.config.install_chara["FileConflicts"] - alreadyExists = os.path.exists(destinationPath) - - if alreadyExists and conflicts == "Skip": - logger.skipped(type, baseName) - return - - elif alreadyExists and conflicts == "Replace": - logger.replaced(type, baseName) - - elif alreadyExists and conflicts == "Rename": - maxRetries = 3 - for attempt in range(maxRetries): - try: - filename, fileExtension = os.path.splitext(sourcePath) - newName = datetime.datetime.now().strftime('%Y%m%d%H%M%S%f') - newSourcePath = f"{filename}_{newName}{fileExtension}" - os.rename(sourcePath, newSourcePath) - sourcePath = newSourcePath - logger.renamed(type, baseName) - - filename, fileExtension = os.path.splitext(destinationPath) - destinationPath = f"{filename}_{newName}{fileExtension}" - break # Exit the loop if renaming is successful - except PermissionError: - if attempt < maxRetries - 1: - time.sleep(1) # Wait for 1 second before retrying - else: - logger.error(type, f"Failed to rename {baseName} after {maxRetries} attempts.") - return - - try: - shutil.copy(sourcePath, destinationPath) - print(f"File copied successfully from {sourcePath} to {destinationPath}") - if not alreadyExists: - logger.success(type, baseName) - except FileNotFoundError: - logger.error(type, f"{baseName} does not exist.") - except PermissionError: - logger.error(type, f"Permission denied for {baseName}") - except Exception as e: - logger.error(type, f"An error occurred: {e}") - - def findAndRemove(self, type, sourcePath, destinationFolder): - sourcePath = sourcePath[0] - baseName = os.path.basename(sourcePath) - destinationPath = os.path.join(destinationFolder, baseName) - if os.path.exists(destinationPath): - try: - os.remove(destinationPath) - logger.removed(type, baseName) - except OSError as e: - logger.error(type, baseName) - - def createArchive(self, folders, archivePath): - # Specify the full path to the 7zip executable - pathTo7zip = patoolib.util.find_program("7z") - if not pathTo7zip: - logger.error("SCRIPT", "7zip not found. Unable to create backup") - raise Exception() - - if os.path.exists(archivePath+".7z"): - os.remove(archivePath+".7z") - - pathTo7zip = f'"{pathTo7zip}"' - archivePath = f'"{archivePath}"' - excludeFolders = [ - '"Sideloader Modpack"', - '"Sideloader Modpack - Studio"', - '"Sideloader Modpack - KK_UncensorSelector"', - '"Sideloader Modpack - Maps"', - '"Sideloader Modpack - KK_MaterialEditor"', - '"Sideloader Modpack - Fixes"', - '"Sideloader Modpack - Exclusive KK KKS"', - '"Sideloader Modpack - Exclusive KK"', - '"Sideloader Modpack - Animations"' - ] - - # Create a string of folder names to exclude - excludeString = '' - for folder in excludeFolders: - excludeString += f'-xr!{folder} ' - - # Create a string of folder names to include - includeString = '' - for folder in folders: - includeString += f'"{folder}" ' - - # Construct the 7zip command - command = f'{pathTo7zip} a -t7z {archivePath} {includeString} {excludeString}' - # Call the command - process = subprocess.run(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - - # Print the output - for line in process.stdout.decode().split('\n'): - if line.strip() != "": - logger.info("7-Zip", line) - # Check the return code - if process.returncode not in [0, 1]: - raise Exception() - - def extractArchive(self, archivePath): - try: - archiveName = os.path.basename(archivePath) - logger.info("ARCHIVE", f"Extracting {archiveName}") - extractPath = os.path.join(f"{os.path.splitext(archivePath)[0]}_{datetime.datetime.now().strftime('%Y%m%d%H%M%S%f')}") - patoolib.extract_archive(archivePath, outdir=extractPath) - return extractPath - - except patoolib.util.PatoolError as e: - text = f"There is an error with the archive {archiveName} but it is impossible to detect the cause. Maybe it requires a password?" - while self.config.install_chara["Password"] == "Request Password": - try: - dialog = customtkinter.CTkInputDialog(text=text, title="Enter Password") - password = dialog.get_input() - - if password is not None or "": - patoolib.extract_archive(archivePath, outdir=extractPath, password=password) - return extractPath - else: - break - except: - text = f"Wrong password or {archiveName} is corrupted. Please enter password again or click Cancel" - - logger.skipped("ARCHIVE", archiveName) - - -fileManager = FileManager() \ No newline at end of file diff --git a/app/common/logger.py b/app/common/logger.py index 85d708c..d66f30b 100644 --- a/app/common/logger.py +++ b/app/common/logger.py @@ -29,8 +29,8 @@ class Logger: # Status Text: INFO, SUCCESS, ERROR, SKIPPED, REPLACED, RENAMED, REMOVED self.status = ['    INFO', '  SUCCESS', '   ERROR', ' SKIPPED', ' REPLACED', ' RENAMED', ' REMOVED'] - # Status Color: Blue, Red, Green, Orange, - self.statusColor = ['#2d8cf0', '#ed3f14', '#f90', '#f90', '#f90', '#f90', '#00c12b'] + # Status Color: Blue, Red, Green, Orange + self.statusColor = ['#2d8cf0', '#00c12b', '#ed3f14', '#f90', '#f90', '#f90', '#f90'] # Status HTML: status self.statusHtml = [ @@ -69,17 +69,20 @@ class Logger: def colorize(self, line): adding = line + print(line) for i, s in enumerate(self.text): if s in line: + print(s) + print(self.statusColor[i]) adding = (f'''
{line}
''') - break - self.logs += adding - self.logger_signal.emit(adding) + self.logs += adding + self.logger_signal.emit(adding) + return def info(self, category: str, message: str) -> None: """ diff --git a/app/common/thread_manager.py b/app/common/thread_manager.py index 540ef0a..e5db576 100644 --- a/app/common/thread_manager.py +++ b/app/common/thread_manager.py @@ -32,8 +32,9 @@ class ThreadManager: logger.colorize(line) def stop(self): - self._script.terminate() - self._script = None + if self._script is not None: + self._script.terminate() + self._script = None threadManager = ThreadManager() diff --git a/app/modules/create_backup.py b/app/modules/create_backup.py deleted file mode 100644 index afe0632..0000000 --- a/app/modules/create_backup.py +++ /dev/null @@ -1,27 +0,0 @@ -import os - -from app.common.file_manager import fileManager -from app.modules.handler import Handler - - -class CreateBackup(Handler): - def __str__(self) -> str: - return "Create Backup" - - def loadConfig(self, config): - super().loadConfig(config) - folders = ["mods", "UserData", "BepInEx"] - self.folders = [self.gamePath[f] for f in folders if self.config[f]] - self.outputPath = self.config["OutputPath"] - self.filename = self.config["Filename"] - - def handle(self, request): - outputPath = os.path.join(self.outputPath, self.filename) - fileManager.createArchive(self.folders, outputPath) - - self.setNext(request) - - - - - \ No newline at end of file diff --git a/app/modules/fc_kks.py b/app/modules/fc_kks.py deleted file mode 100644 index b363c92..0000000 --- a/app/modules/fc_kks.py +++ /dev/null @@ -1,108 +0,0 @@ -import os -import re as regex -import codecs -import shutil - -from app.common.logger import logger -from app.modules.handler import Handler - - -class FilterConvertKKS(Handler): - def __str__(self) -> str: - return "Filter Convert KKS" - - def loadConfig(self, config): - super().loadConfig(config) - self.convert = self.config["Convert"] - - def getList(self, folderPath): - newList = [] - for root, dirs, files in os.walk(folderPath): - for filename in files: - if regex.match(r".*(\.png)$", filename): - newList.append(os.path.join(root, filename)) - return newList - - def checkPng(self, cardPath): - with codecs.open(cardPath, "rb") as card: - data = card.read() - cardType = 0 - if data.find(b"KoiKatuChara") != -1: - cardType = 1 - if data.find(b"KoiKatuCharaSP") != -1: - cardType = 2 - elif data.find(b"KoiKatuCharaSun") != -1: - cardType = 3 - logger.info(f"[{cardType}]", f"{cardPath}") - return cardType - - def convertKk(self, cardName, cardPath, destinationPath): - with codecs.open(cardPath, mode="rb") as card: - data = card.read() - - replaceList = [ - [b"\x15\xe3\x80\x90KoiKatuCharaSun", b"\x12\xe3\x80\x90KoiKatuChara"], - [b"Parameter\xa7version\xa50.0.6", b"Parameter\xa7version\xa50.0.5"], - [b"version\xa50.0.6\xa3sex", b"version\xa50.0.5\xa3sex"], - ] - - for text in replaceList: - data = data.replace(text[0], text[1]) - - newFilePath = os.path.normpath(os.path.join(destinationPath, f"KKS2KK_{cardName}")) - - with codecs.open(newFilePath, "wb") as newCard: - newCard.write(data) - - def handle(self, request): - path = self.config.fc_kks["InputPath"] - kksCardList = [] - kksFolder = "_KKS_card_" - kksFolder2 = "_KKS_to_KK_" - - pngList = self.getList(path) - - count = len(pngList) - if count > 0: - logger.info("SCRIPT", "0: unknown / 1: kk / 2: kksp / 3: kks") - for png in pngList: - if self.checkPng(png) == 3: - kksCardList.append(png) - else: - logger.success("SCRIPT", f"no PNG found") - return - - count = len(kksCardList) - if count > 0: - print(kksCardList) - - targetFolder = os.path.normpath(os.path.join(path, kksFolder)) - targetFolder2 = os.path.normpath(os.path.join(path, kksFolder2)) - if not os.path.isdir(targetFolder): - os.mkdir(targetFolder) - - if self.convert: - logger.info("SCRIPT", f"Conversion to KK is [{self.convert}]") - if not os.path.isdir(targetFolder2): - os.mkdir(targetFolder2) - - for cardPath in kksCardList: - source = cardPath - card = os.path.basename(cardPath) - target = os.path.normpath(os.path.join(targetFolder, card)) - - # copy & convert before move - if self.convert: - self.convertKk(card, source, targetFolder2) - - # move file - shutil.move(source, target) - - if self.convert: - logger.success("SCRIPT", f"[{count}] cards moved to [{kksFolder}] folder, converted and save to [{kksFolder2}] folder") - else: - logger.success("SCRIPT", f"[{count}] cards moved to [{kksFolder}] folder") - else: - logger.success("SCRIPT", f"no KKS card found") - - self.setNext(request) \ No newline at end of file diff --git a/app/modules/handler.py b/app/modules/handler.py deleted file mode 100644 index 3a719ff..0000000 --- a/app/modules/handler.py +++ /dev/null @@ -1,16 +0,0 @@ -class Handler: - def __str__(self) -> str: - return "Handler" - - def loadConfig(self, config): - self.gamePath = config.get("Core", "GamePath") - key = str(self).replace(" ", "") - self.config = config.get(key) - - def handle(self, request): - pass - - def setNext(self, request): - request.removeHandler() - request.process() - diff --git a/app/modules/install_chara.py b/app/modules/install_chara.py deleted file mode 100644 index 3f9d1bb..0000000 --- a/app/modules/install_chara.py +++ /dev/null @@ -1,57 +0,0 @@ -import os -import codecs - -from app.common.file_manager import fileManager -from app.common.logger import logger -from app.modules.handler import Handler - - -class InstallChara(Handler): - def __str__(self) -> str: - return "Install Chara" - - def loadConfig(self, config): - super().loadConfig(config) - self.inputPath = self.config["InputPath"] - - def resolvePng(self, imagePath): - with codecs.open(imagePath[0], "rb") as card: - data = card.read() - if data.find(b"KoiKatuChara") != -1: - if data.find(b"KoiKatuCharaSP") != -1 or data.find(b"KoiKatuCharaSun") != -1: - basename = os.path.basename(imagePath[0]) - logger.error("CHARA", f"{basename} is a KKS card") - return - fileManager.copyAndPaste("CHARA", imagePath, self.gamePath["chara"]) - elif data.find(b"KoiKatuClothes") != -1: - fileManager.copyAndPaste("COORD",imagePath, self.gamePath["coordinate"]) - else: - fileManager.copyAndPaste("OVERLAYS", imagePath, self.gamePath["Overlays"]) - - def handle(self, request, folderPath=None): - if folderPath is None: - folderPath = self.inputPath - foldername = os.path.basename(folderPath) - logger.log_msg("FOLDER", foldername) - fileList, archiveList = fileManager.findAllFiles(folderPath) - - for file in fileList: - extension = file[2] - match extension: - case ".zipmod": - fileManager.copyAndPaste("MODS", file, self.gamePath["mods"]) - case ".png": - self.resolvePng(file) - case _: - basename = os.path.basename(file[0]) - logger.error("UKNOWN", f"Cannot classify {basename}") - - for archive in archiveList: - extractPath = fileManager.extractArchive(archive[0]) - if extractPath is not None: - self.handle(extractPath) - - self.setNext(request) - - - \ No newline at end of file diff --git a/app/modules/remove_chara.py b/app/modules/remove_chara.py deleted file mode 100644 index 792fa58..0000000 --- a/app/modules/remove_chara.py +++ /dev/null @@ -1,48 +0,0 @@ -import os -import codecs - -from app.common.file_manager import fileManager -from app.common.logger import logger -from app.modules.handler import Handler - - -class RemoveChara(Handler): - def __str__(self) -> str: - return "Remove Chara" - - def loadConfig(self, config): - super().configLoad(config) - self.inputPath = self.config["InputPath"] - - def resolvePng(self, imagePath): - with codecs.open(imagePath[0], "rb") as card: - data = card.read() - if data.find(b"KoiKatuChara") != -1: - if data.find(b"KoiKatuCharaSP") != -1 or data.find(b"KoiKatuCharaSun") != -1: - return - fileManager.findAndRemove("CHARA", imagePath, self.gamePath["chara"]) - elif data.find(b"KoiKatuClothes") != -1: - fileManager.findAndRemove("COORD",imagePath, self.gamePath["coordinate"]) - else: - fileManager.findAndRemove("OVERLAYS", imagePath, self.gamePath["Overlays"]) - - def handle(self, request): - foldername = os.path.basename(self.inputPath) - logger.info("FOLDER", foldername) - fileList, archiveList = fileManager.findAllFiles(self.inputPath) - - for file in fileList: - extension = file[2] - match extension: - case ".zipmod": - fileManager.findAndRemove("MODS", file, self.game_path["mods"]) - case ".png": - self.resolvePng(file) - case _: - pass - - logger.line() - self.setNext(request) - - - \ No newline at end of file diff --git a/app/modules/request.py b/app/modules/request.py deleted file mode 100644 index 6a03058..0000000 --- a/app/modules/request.py +++ /dev/null @@ -1,76 +0,0 @@ -import os - -from app.common.config import cfg -from app.common.logger import logger - -from app.modules.handler import Handler -from app.modules.create_backup import CreateBackup -from app.modules.fc_kks import FilterConvertKKS -from app.modules.install_chara import InstallChara -from app.modules.remove_chara import RemoveChara - - -class Request: - def __init__(self): - self._handlers = [CreateBackup(), FilterConvertKKS(), InstallChara(), RemoveChara()] - self._config = cfg.toDict() - self._isValid = True - - logger.info("SCRIPT", "Validating config") - self.validateGamepath() - self._handlers = [x for x in self.handlers if self.isTaskEnabled(x)] - - if not self._handlers: - logger.error("SCRIPT", "No task enabled") - if not self._isValid: - raise Exception() - - def validatePath(self, path, errorMsg): - if not os.path.exists(path): - logger.error("SCRIPT", errorMsg) - self._isValid = False - return False - return True - - def validateGamepath(self): - base = self.config['Core']['GamePath'] - self.config['Core']['GamePath'] = { - "base": base, - "UserData": os.path.join(base, "UserData"), - "BepInEx": os.path.join(base, "BepInEx"), - "mods": os.path.join(base, "mods"), - "chara": os.path.join(base, "UserData\\chara\\female"), - "coordinate": os.path.join(base, "UserData\\coordinate"), - "Overlays": os.path.join(base, "UserData\\Overlays") - } - - for directory, path in self.config['Core']['GamePath'].items(): - self.validatePath(path, f"Game path not valid. Missing {directory} directory.") - - def isTaskEnabled(self, handler: Handler): - task = str(handler).replace(" ", "") - taskConfig = self.config[task] - - if not taskConfig["Enable"]: - return False - - if (path := taskConfig.get("InputPath")): - self.validatePath(path, f"Invalid path for {str(handler)}: {path}") - - if (path := taskConfig.get("OutputPath")): - self.validatePath(path, f"Invalid path for {str(handler)}: {path}") - - if self._isValid: - handler.loadConfig(self.config) - - return True - - def removeHandler(self) -> Handler: - if len(self._handlers) > 1: - return self._handlers.pop(0) - - def process(self): - if self._handlers: - self._handlers[0].handle() - - diff --git a/app/view/setting_interface.py b/app/view/setting_interface.py index ed712ff..221eca2 100644 --- a/app/view/setting_interface.py +++ b/app/view/setting_interface.py @@ -8,7 +8,7 @@ from qfluentwidgets import FluentIcon as FIF from qfluentwidgets import InfoBar, DisplayLabel from PySide6.QtCore import Qt, QUrl from PySide6.QtGui import QDesktopServices -from PySide6.QtWidgets import QWidget +from PySide6.QtWidgets import QWidget, QFileDialog from ..common.config import cfg, HELP_URL, FEEDBACK_URL, AUTHOR, VERSION, YEAR, isWin11 from ..components.line_edit_card import LineEditSettingCard @@ -307,23 +307,29 @@ class SettingInterface(ScrollArea): parent=self ) - # def __onDownloadFolderCardClicked(self): - # """ download folder card clicked slot """ - # folder = QFileDialog.getExistingDirectory( - # self, self.tr("Choose folder"), "./") - # if not folder or cfg.get(cfg.downloadFolder) == folder: - # return + def __onFolderCardClicked(self, item, card): + """ download folder card clicked slot """ + folder = QFileDialog.getExistingDirectory( + self, self.tr("Choose folder"), "./") + if not folder or cfg.get(item) == folder: + return - # cfg.set(cfg.downloadFolder, folder) - # self.downloadFolderCard.setContent(folder) + cfg.set(item, folder) + card.setContent(folder) def __connectSignalToSlot(self): """ connect signal to slot """ cfg.appRestartSig.connect(self.__showRestartTooltip) - - # music in the pc - # self.downloadFolderCard.clicked.connect( - # self.__onDownloadFolderCardClicked) + # gamePath + self.gamePathCard.clicked.connect(lambda: self.__onFolderCardClicked(cfg.gamePath, self.gamePathCard)) + # backup + self.backupPathCard.clicked.connect(lambda: self.__onFolderCardClicked(cfg.backupPath, self.backupPathCard)) + # fckks + self.fckksPathCard.clicked.connect(lambda: self.__onFolderCardClicked(cfg.fccksPath, self.fckksPathCard)) + # install + self.installPathCard.clicked.connect(lambda: self.__onFolderCardClicked(cfg.installPath, self.installPathCard)) + # remove + self.removePathCard.clicked.connect(lambda: self.__onFolderCardClicked(cfg.removePath, self.removePathCard)) # personalization self.themeCard.optionChanged.connect(lambda ci: setTheme(cfg.get(ci))) diff --git a/app/modules/__init__.py b/modules/__init__.py similarity index 100% rename from app/modules/__init__.py rename to modules/__init__.py diff --git a/modules/create_backup.py b/modules/create_backup.py new file mode 100644 index 0000000..8ef0d08 --- /dev/null +++ b/modules/create_backup.py @@ -0,0 +1,26 @@ +import os + +class CreateBackup: + def __init__(self, config, file_manager): + """Initializes the Bounty module. + + Args: + config (Config): BAAuto Config instance + """ + self.config = config + self.file_manager = file_manager + self.game_path = self.config.game_path + folders = ["mods", "UserData", "BepInEx"] + self.folders = [self.game_path[f] for f in folders if self.config.create_backup[f]] + self.filename = self.config.create_backup["Filename"] + self.output_path = self.config.create_backup["OutputPath"] + + def logic_wrapper(self): + output_path = os.path.join(self.output_path, self.filename) + self.file_manager.create_archive(self.folders, output_path) + + + + + + \ No newline at end of file diff --git a/modules/fc_kks.py b/modules/fc_kks.py new file mode 100644 index 0000000..992d041 --- /dev/null +++ b/modules/fc_kks.py @@ -0,0 +1,107 @@ +import os +import re as regex +import codecs +import shutil +from util.logger import Logger + +class FilterConvertKKS: + def __init__(self, config, file_manager): + """Initializes the Bounty module. + + Args: + config (Config): BAAuto Config instance + """ + self.config = config + self.file_manager = file_manager + self.convert = self.config.fc_kks["Convert"] + + def get_list(self, folder_path): + new_list = [] + for root, dirs, files in os.walk(folder_path): + for filename in files: + if regex.match(r".*(\.png)$", filename): + new_list.append(os.path.join(root, filename)) + return new_list + + def check_png(self, card_path): + with codecs.open(card_path, "rb") as card: + data = card.read() + card_type = 0 + if data.find(b"KoiKatuChara") != -1: + card_type = 1 + if data.find(b"KoiKatuCharaSP") != -1: + card_type = 2 + elif data.find(b"KoiKatuCharaSun") != -1: + card_type = 3 + Logger.log_info(f"[{card_type}]", f"{card_path}") + return card_type + + def convert_kk(self, card_name, card_path, destination_path): + with codecs.open(card_path, mode="rb") as card: + data = card.read() + + replace_list = [ + [b"\x15\xe3\x80\x90KoiKatuCharaSun", b"\x12\xe3\x80\x90KoiKatuChara"], + [b"Parameter\xa7version\xa50.0.6", b"Parameter\xa7version\xa50.0.5"], + [b"version\xa50.0.6\xa3sex", b"version\xa50.0.5\xa3sex"], + ] + + for text in replace_list: + data = data.replace(text[0], text[1]) + + new_file_path = os.path.normpath(os.path.join(destination_path, f"KKS2KK_{card_name}")) + # print(f"new_file_path {new_file_path}") + + with codecs.open(new_file_path, "wb") as new_card: + new_card.write(data) + + def logic_wrapper(self): + path = self.config.fc_kks["InputPath"] + kks_card_list = [] + kks_folder = "_KKS_card_" + kks_folder2 = "_KKS_to_KK_" + + png_list = self.get_list(path) + + count = len(png_list) + if count > 0: + Logger.log_info("SCRIPT", "0: unknown / 1: kk / 2: kksp / 3: kks") + for png in png_list: + if self.check_png(png) == 3: + kks_card_list.append(png) + else: + Logger.log_success("SCRIPT", f"no PNG found") + return + + count = len(kks_card_list) + if count > 0: + print(kks_card_list) + + target_folder = os.path.normpath(os.path.join(path, kks_folder)) + target_folder2 = os.path.normpath(os.path.join(path, kks_folder2)) + if not os.path.isdir(target_folder): + os.mkdir(target_folder) + + if self.convert: + Logger.log_info("SCRIPT", f"Conversion to KK is [{self.convert}]") + if not os.path.isdir(target_folder2): + os.mkdir(target_folder2) + + for card_path in kks_card_list: + source = card_path + card = os.path.basename(card_path) + target = os.path.normpath(os.path.join(target_folder, card)) + + # copy & convert before move + if self.convert: + self.convert_kk(card, source, target_folder2) + + # move file + shutil.move(source, target) + + if self.convert: + Logger.log_success("SCRIPT", f"[{count}] cards moved to [{kks_folder}] folder, converted and save to [{kks_folder2}] folder") + else: + Logger.log_success("SCRIPT", f"[{count}] cards moved to [{kks_folder}] folder") + else: + Logger.log_success("SCRIPT", f"no KKS card found") diff --git a/modules/install_chara.py b/modules/install_chara.py new file mode 100644 index 0000000..05f12b1 --- /dev/null +++ b/modules/install_chara.py @@ -0,0 +1,56 @@ +import os +import codecs +from util.logger import Logger + +class InstallChara: + def __init__(self, config, file_manager): + """Initializes the Bounty module. + + Args: + config (Config): BAAuto Config instance + """ + self.config = config + self.file_manager = file_manager + self.game_path = self.config.game_path + self.input_path = self.config.install_chara["InputPath"] + + def resolve_png(self, image_path): + with codecs.open(image_path[0], "rb") as card: + data = card.read() + if data.find(b"KoiKatuChara") != -1: + if data.find(b"KoiKatuCharaSP") != -1 or data.find(b"KoiKatuCharaSun") != -1: + basename = os.path.basename(image_path[0]) + Logger.log_error("CHARA", f"{basename} is a KKS card") + return + self.file_manager.copy_and_paste("CHARA", image_path, self.game_path["chara"]) + elif data.find(b"KoiKatuClothes") != -1: + self.file_manager.copy_and_paste("COORD",image_path, self.game_path["coordinate"]) + else: + self.file_manager.copy_and_paste("OVERLAYS", image_path, self.game_path["Overlays"]) + + def logic_wrapper(self, folder_path=None): + if folder_path is None: + folder_path = self.input_path + foldername = os.path.basename(folder_path) + Logger.log_msg("FOLDER", foldername) + file_list, compressed_file_list = self.file_manager.find_all_files(folder_path) + + for file in file_list: + file_extension = file[2] + match file_extension: + case ".zipmod": + self.file_manager.copy_and_paste("MODS", file, self.game_path["mods"]) + case ".png": + self.resolve_png(file) + case _: + basename = os.path.basename(file[0]) + Logger.log_error("UKNOWN", f"Cannot classify {basename}") + print("[MSG]") + + for compressed in compressed_file_list: + extract_path = self.file_manager.extract_archive(compressed[0]) + if extract_path is not None: + self.logic_wrapper(extract_path) + + + \ No newline at end of file diff --git a/modules/remove_chara.py b/modules/remove_chara.py new file mode 100644 index 0000000..a80d5f2 --- /dev/null +++ b/modules/remove_chara.py @@ -0,0 +1,47 @@ +import os +import codecs +from util.logger import Logger + +class RemoveChara: + def __init__(self, config, file_manager): + """Initializes the Bounty module. + + Args: + config (Config): BAAuto Config instance + """ + self.config = config + self.file_manager = file_manager + self.game_path = self.config.game_path + self.input_path = self.config.remove_chara["InputPath"] + + def resolve_png(self, image_path): + with codecs.open(image_path[0], "rb") as card: + data = card.read() + if data.find(b"KoiKatuChara") != -1: + if data.find(b"KoiKatuCharaSP") != -1 or data.find(b"KoiKatuCharaSun") != -1: + return + self.file_manager.find_and_remove("CHARA", image_path, self.game_path["chara"]) + elif data.find(b"KoiKatuClothes") != -1: + self.file_manager.find_and_remove("COORD",image_path, self.game_path["coordinate"]) + else: + self.file_manager.find_and_remove("OVERLAYS", image_path, self.game_path["Overlays"]) + + def logic_wrapper(self): + foldername = os.path.basename(self.input_path) + Logger.log_msg("FOLDER", foldername) + file_list, archive_list = self.file_manager.find_all_files(self.input_path) + + for file in file_list: + file_extension = file[2] + match file_extension: + case ".zipmod": + self.file_manager.find_and_remove("MODS", file, self.game_path["mods"]) + case ".png": + self.resolve_png(file) + case _: + pass + print("[MSG]") + + + + \ No newline at end of file diff --git a/script.py b/script.py index 5a44438..a83096f 100644 --- a/script.py +++ b/script.py @@ -1,5 +1,69 @@ -from app.modules.request import Request +import sys +import traceback +try: + with open('traceback.log', 'w') as f: + pass + + from util.config import Config + from util.logger import Logger + from util.file_manager import FileManager + from modules.install_chara import InstallChara + from modules.remove_chara import RemoveChara + from modules.fc_kks import FilterConvertKKS + from modules.create_backup import CreateBackup -request = Request() -request.process() \ No newline at end of file + class Script: + def __init__(self, config, file_manager): + """Initializes the primary azurlane-auto instance with the passed in + Config instance; + + Args: + config (Config): BAAuto Config instance + """ + self.config = config + self.file_manager = file_manager + self.modules = { + 'InstallChara': None, + 'RemoveChara': None, + 'CreateBackup': None, + 'FilterConvertKKS': None, + } + if self.config.install_chara['Enable']: + self.modules['InstallChara'] = InstallChara(self.config, self.file_manager) + if self.config.remove_chara['Enable']: + self.modules['RemoveChara'] = RemoveChara(self.config, self.file_manager) + if self.config.create_backup['Enable']: + self.modules['CreateBackup'] = CreateBackup(self.config, self.file_manager) + if self.config.fc_kks["Enable"]: + self.modules['FilterConvertKKS'] = FilterConvertKKS(self.config, self.file_manager) + + def run(self): + for task in self.config.tasks: + if self.modules[task]: + Logger.log_info("SCRIPT", f'Start Task: {task}') + try: + self.modules[task].logic_wrapper() + except: + Logger.log_error("SCRIPT", f'Task error: {task}. For more info, check the traceback.log file.') + with open('traceback.log', 'a') as f: + f.write(f'[{task}]\n') + traceback.print_exc(None, f, True) + f.write('\n') + sys.exit(1) + sys.exit(0) + +except: + print(f'[ERROR] Script Initialisation Error. For more info, check the traceback.log file.') + with open('traceback.log', 'w') as f: + f.write(f'Script Initialisation Error\n') + traceback.print_exc(None, f, True) + f.write('\n') + sys.exit(1) + + +if __name__ == "__main__": + config = Config('app/config/config.json') + file_manager = FileManager(config) + script = Script(config, file_manager) + script.run() diff --git a/util/__init__.py b/util/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/util/config.py b/util/config.py new file mode 100644 index 0000000..6c70642 --- /dev/null +++ b/util/config.py @@ -0,0 +1,90 @@ +import sys +import json +import os +from util.logger import Logger + +class Config: + def __init__(self, config_file): + Logger.log_info("SCRIPT", "Initializing config module") + self.config_file = config_file + self.ok = False + self.initialized = False + self.config_data = None + self.changed = False + self.read() + + def read(self): + backup_config = self._deepcopy_dict(self.__dict__) + + try: + with open(self.config_file, 'r') as json_file: + self.config_data = json.load(json_file) + except FileNotFoundError: + Logger.log_error("SCRIPT", f"Config file '{self.config_file}' not found.") + sys.exit(1) + except json.JSONDecodeError: + Logger.log_error("SCRIPT", f"Invalid JSON format in '{self.config_file}'.") + sys.exit(1) + + self.validate() + + if self.ok and not self.initialized: + Logger.log_info("SCRIPT", "Starting KKAFIO!") + self.initialized = True + self.changed = True + elif not self.ok and not self.initialized: + Logger.log_error("SCRIPT", "Invalid config. Please check your config file.") + sys.exit(1) + elif not self.ok and self.initialized: + Logger.log_warning("SCRIPT", "Config change detected, but with problems. Rolling back config.") + self._rollback_config(backup_config) + elif self.ok and self.initialized: + if backup_config != self.__dict__: + Logger.log_warning("SCRIPT", "Config change detected. Hot-reloading.") + self.changed = True + + def validate(self): + Logger.log_info("SCRIPT", "Validating config") + self.ok = True + self.tasks = ["CreateBackup", "FilterConvertKKS", "InstallChara", "RemoveChara"] + self.create_gamepath() + + for task in self.tasks: + if self.config_data[task]["Enable"]: + if "InputPath" in self.config_data[task]: + path = self.config_data[task]["InputPath"] + elif "OutputPath" in self.config_data[task]: + path = self.config_data[task]["OutputPath"] + if not os.path.exists(path): + Logger.log_error("SCRIPT", f"Path invalid for task {task}") + raise Exception() + + self.install_chara = self.config_data.get("InstallChara", {}) + self.create_backup = self.config_data.get("CreateBackup", {}) + self.remove_chara = self.config_data.get("RemoveChara", {}) + self.fc_kks = self.config_data.get("FilterConvertKKS", {}) + + def create_gamepath(self): + base = self.config_data["Core"]["GamePath"] + self.game_path = { + "base": base, + "UserData": os.path.join(base, "UserData"), + "BepInEx": os.path.join(base, "BepInEx"), + "mods": os.path.join(base, "mods"), + "chara": os.path.join(base, "UserData\\chara\\female"), + "coordinate": os.path.join(base, "UserData\coordinate"), + "Overlays": os.path.join(base, "UserData\Overlays") + } + + for path in self.game_path.values(): + if not os.path.exists(path): + Logger.log_error("SCRIPT", "Game path not valid") + raise Exception() + + def _deepcopy_dict(self, dictionary): + from copy import deepcopy + return deepcopy(dictionary) + + def _rollback_config(self, config): + for key, value in config.items(): + setattr(self, key, value) diff --git a/util/file_manager.py b/util/file_manager.py new file mode 100644 index 0000000..89e379a --- /dev/null +++ b/util/file_manager.py @@ -0,0 +1,165 @@ +import os +import shutil +import datetime +import patoolib +import customtkinter +import subprocess +import time +from util.logger import Logger + +class FileManager: + + def __init__(self, config): + self.config = config + + def find_all_files(self, directory): + file_list = [] + compressed_file_list = [] + compressed_extensions = [".rar", ".zip", ".7z"] + + for root, dirs, files in os.walk(directory): + for file in files: + + file_path = os.path.join(root, file) + file_size = os.path.getsize(file_path) + _, file_extension = os.path.splitext(file_path) + + if file_extension in compressed_extensions: + compressed_file_list.append((file_path, file_size, file_extension)) + else: + file_list.append((file_path, file_size, file_extension)) + + file_list.sort(key=lambda x: x[1]) + compressed_file_list.sort(key=lambda x: x[1]) + return file_list, compressed_file_list + + def copy_and_paste(self, type, source_path, destination_folder): + source_path = source_path[0] + base_name = os.path.basename(source_path) + destination_path = os.path.join(destination_folder, base_name) + conflicts = self.config.install_chara["FileConflicts"] + already_exists = os.path.exists(destination_path) + + if already_exists and conflicts == "Skip": + Logger.log_skipped(type, base_name) + return + + elif already_exists and conflicts == "Replace": + Logger.log_replaced(type, base_name) + + elif already_exists and conflicts == "Rename": + max_retries = 3 + for attempt in range(max_retries): + try: + filename, file_extension = os.path.splitext(source_path) + new_name = datetime.datetime.now().strftime('%Y%m%d%H%M%S%f') + new_source_path = f"{filename}_{new_name}{file_extension}" + os.rename(source_path, new_source_path) + source_path = new_source_path + Logger.log_renamed(type, base_name) + + filename, file_extension = os.path.splitext(destination_path) + destination_path = f"{filename}_{new_name}{file_extension}" + break # Exit the loop if renaming is successful + except PermissionError: + if attempt < max_retries - 1: + time.sleep(1) # Wait for 1 second before retrying + else: + Logger.log_error(type, f"Failed to rename {base_name} after {max_retries} attempts.") + return + + try: + shutil.copy(source_path, destination_path) + print(f"File copied successfully from {source_path} to {destination_path}") + if not already_exists: + Logger.log_success(type, base_name) + except FileNotFoundError: + Logger.log_error(type, f"{base_name} does not exist.") + except PermissionError: + Logger.log_error(type, f"Permission denied for {base_name}") + except Exception as e: + Logger.log_error(type, f"An error occurred: {e}") + + def find_and_remove(self, type, source_path, destination_folder): + source_path = source_path[0] + base_name = os.path.basename(source_path) + destination_path = os.path.join(destination_folder, base_name) + if os.path.exists(destination_path): + try: + os.remove(destination_path) + Logger.log_removed(type, base_name) + except OSError as e: + Logger.log_error(type, base_name) + + def create_archive(self, folders, archive_path): + # Specify the full path to the 7zip executable + path_to_7zip = patoolib.util.find_program("7z") + if not path_to_7zip: + Logger.log_error("SCRIPT", "7zip not found. Unable to create backup") + raise Exception() + + if os.path.exists(archive_path+".7z"): + os.remove(archive_path+".7z") + + path_to_7zip = f'"{path_to_7zip}"' + archive_path = f'"{archive_path}"' + exclude_folders = [ + '"Sideloader Modpack"', + '"Sideloader Modpack - Studio"', + '"Sideloader Modpack - KK_UncensorSelector"', + '"Sideloader Modpack - Maps"', + '"Sideloader Modpack - KK_MaterialEditor"', + '"Sideloader Modpack - Fixes"', + '"Sideloader Modpack - Exclusive KK KKS"', + '"Sideloader Modpack - Exclusive KK"', + '"Sideloader Modpack - Animations"' + ] + + # Create a string of folder names to exclude + exclude_string = '' + for folder in exclude_folders: + exclude_string += f'-xr!{folder} ' + + # Create a string of folder names to include + include_string = '' + for folder in folders: + include_string += f'"{folder}" ' + + # Construct the 7zip command + command = f'{path_to_7zip} a -t7z {archive_path} {include_string} {exclude_string}' + # Call the command + process = subprocess.run(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + # Print the output + for line in process.stdout.decode().split('\n'): + if line.strip() != "": + Logger.log_info("7-Zip", line) + # Check the return code + if process.returncode not in [0, 1]: + raise Exception() + + def extract_archive(self, archive_path): + try: + archive_name = os.path.basename(archive_path) + Logger.log_info("ARCHIVE", f"Extracting {archive_name}") + extract_path = os.path.join(f"{os.path.splitext(archive_path)[0]}_{datetime.datetime.now().strftime('%Y%m%d%H%M%S%f')}") + patoolib.extract_archive(archive_path, outdir=extract_path) + return extract_path + + except patoolib.util.PatoolError as e: + text = f"There is an error with the archive {archive_name} but it is impossible to detect the cause. Maybe it requires a password?" + while self.config.install_chara["Password"] == "Request Password": + try: + dialog = customtkinter.CTkInputDialog(text=text, title="Enter Password") + password = dialog.get_input() + + if password is not None or "": + patoolib.extract_archive(archive_path, outdir=extract_path, password=password) + return extract_path + else: + break + except: + text = f"Wrong password or {archive_name} is corrupted. Please enter password again or click Cancel" + + Logger.log_skipped("ARCHIVE", archive_name) + diff --git a/util/logger.py b/util/logger.py new file mode 100644 index 0000000..36c38b8 --- /dev/null +++ b/util/logger.py @@ -0,0 +1,34 @@ +class Logger(object): + + @classmethod + def log_msg(cls, type, msg): + print(f"[MSG][{type}] {msg}") + + @classmethod + def log_success(cls, type, msg): + print(f"[SUCCESS][{type}] {msg}") + + @classmethod + def log_skipped(cls, type, msg): + print(f"[SKIPPED][{type}] {msg}") + + @classmethod + def log_replaced(cls, type, msg): + print(f"[REPLACED][{type}] {msg}") + + @classmethod + def log_renamed(cls, type, msg): + print(f"[RENAMED][{type}] {msg}") + + @classmethod + def log_removed(cls, type, msg): + print(f"[REMOVED][{type}] {msg}") + + @classmethod + def log_error(cls, type, msg): + print(f"[ERROR][{type}] {msg}") + + @classmethod + def log_info(cls, type, msg): + print(f"[INFO][{type}] {msg}") +