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}")
+