mirror of
https://github.com/RedDeadDepresso/KKAFIO.git
synced 2025-12-22 09:20:02 +00:00
revert: modules
This commit is contained in:
@@ -36,17 +36,17 @@ class Config(QConfig):
|
|||||||
""" Config of application """
|
""" Config of application """
|
||||||
|
|
||||||
# core
|
# core
|
||||||
gamePath = ConfigItem("Core", "GamePath", "", FolderValidator())
|
gamePath = ConfigItem("Core", "GamePath", "C:/Program Files (x86)/Steam/steamapps/common/Koikatsu Party", FolderValidator())
|
||||||
|
|
||||||
# createBackup
|
# createBackup
|
||||||
backupEnable = ConfigItem(
|
backupEnable = ConfigItem(
|
||||||
"CreateBackup", "Enable", False, BoolValidator()
|
"CreateBackup", "Enable", False, BoolValidator()
|
||||||
)
|
)
|
||||||
backupPath = ConfigItem(
|
backupPath = ConfigItem(
|
||||||
"CreateBackup", "OutputPath", "", FolderValidator()
|
"CreateBackup", "OutputPath", "C:/Backup", FolderValidator()
|
||||||
)
|
)
|
||||||
filename = ConfigItem(
|
filename = ConfigItem(
|
||||||
"CreateBackup", "Filename", "",
|
"CreateBackup", "Filename", "koikatsu_backup",
|
||||||
)
|
)
|
||||||
userData = ConfigItem(
|
userData = ConfigItem(
|
||||||
"CreateBackup", "UserData", False, BoolValidator()
|
"CreateBackup", "UserData", False, BoolValidator()
|
||||||
@@ -73,7 +73,7 @@ class Config(QConfig):
|
|||||||
installEnable = ConfigItem(
|
installEnable = ConfigItem(
|
||||||
"InstallChara", "Enable", False, BoolValidator()
|
"InstallChara", "Enable", False, BoolValidator()
|
||||||
)
|
)
|
||||||
gamePath = ConfigItem(
|
installPath = ConfigItem(
|
||||||
"InstallChara", "InputPath", "", FolderValidator())
|
"InstallChara", "InputPath", "", FolderValidator())
|
||||||
fileConflicts = OptionsConfigItem(
|
fileConflicts = OptionsConfigItem(
|
||||||
"InstallChara", "FileConflicts", "Skip", OptionsValidator(["Skip", "Replace", "Rename"])
|
"InstallChara", "FileConflicts", "Skip", OptionsValidator(["Skip", "Replace", "Rename"])
|
||||||
@@ -86,8 +86,8 @@ class Config(QConfig):
|
|||||||
removeEnable = ConfigItem(
|
removeEnable = ConfigItem(
|
||||||
"RemoveChara", "Enable", False, BoolValidator()
|
"RemoveChara", "Enable", False, BoolValidator()
|
||||||
)
|
)
|
||||||
gamePath = ConfigItem(
|
removePath = ConfigItem(
|
||||||
"InstallChara", "InputPath", "", FolderValidator())
|
"RemoveChara", "InputPath", "", FolderValidator())
|
||||||
|
|
||||||
# main window
|
# main window
|
||||||
micaEnabled = ConfigItem("MainWindow", "MicaEnabled", isWin11(), BoolValidator())
|
micaEnabled = ConfigItem("MainWindow", "MicaEnabled", isWin11(), BoolValidator())
|
||||||
|
|||||||
@@ -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()
|
|
||||||
@@ -29,8 +29,8 @@ class Logger:
|
|||||||
# Status Text: INFO, SUCCESS, ERROR, SKIPPED, REPLACED, RENAMED, REMOVED
|
# Status Text: INFO, SUCCESS, ERROR, SKIPPED, REPLACED, RENAMED, REMOVED
|
||||||
self.status = [' INFO', ' SUCCESS', ' ERROR',
|
self.status = [' INFO', ' SUCCESS', ' ERROR',
|
||||||
' SKIPPED', ' REPLACED', ' RENAMED', ' REMOVED']
|
' SKIPPED', ' REPLACED', ' RENAMED', ' REMOVED']
|
||||||
# Status Color: Blue, Red, Green, Orange,
|
# Status Color: Blue, Red, Green, Orange
|
||||||
self.statusColor = ['#2d8cf0', '#ed3f14', '#f90', '#f90', '#f90', '#f90', '#00c12b']
|
self.statusColor = ['#2d8cf0', '#00c12b', '#ed3f14', '#f90', '#f90', '#f90', '#f90']
|
||||||
|
|
||||||
# Status HTML: <b style="color:$color">status</b>
|
# Status HTML: <b style="color:$color">status</b>
|
||||||
self.statusHtml = [
|
self.statusHtml = [
|
||||||
@@ -69,17 +69,20 @@ class Logger:
|
|||||||
|
|
||||||
def colorize(self, line):
|
def colorize(self, line):
|
||||||
adding = line
|
adding = line
|
||||||
|
print(line)
|
||||||
for i, s in enumerate(self.text):
|
for i, s in enumerate(self.text):
|
||||||
if s in line:
|
if s in line:
|
||||||
|
print(s)
|
||||||
|
print(self.statusColor[i])
|
||||||
adding = (f'''
|
adding = (f'''
|
||||||
<div style="font-family: Consolas, monospace;color:{self.statusColor[i]};">
|
<div style="font-family: Consolas, monospace;color:{self.statusColor[i]};">
|
||||||
{line}
|
{line}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
''')
|
''')
|
||||||
break
|
self.logs += adding
|
||||||
self.logs += adding
|
self.logger_signal.emit(adding)
|
||||||
self.logger_signal.emit(adding)
|
return
|
||||||
|
|
||||||
def info(self, category: str, message: str) -> None:
|
def info(self, category: str, message: str) -> None:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -32,8 +32,9 @@ class ThreadManager:
|
|||||||
logger.colorize(line)
|
logger.colorize(line)
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
self._script.terminate()
|
if self._script is not None:
|
||||||
self._script = None
|
self._script.terminate()
|
||||||
|
self._script = None
|
||||||
|
|
||||||
|
|
||||||
threadManager = ThreadManager()
|
threadManager = ThreadManager()
|
||||||
|
|||||||
@@ -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)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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)
|
|
||||||
@@ -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()
|
|
||||||
|
|
||||||
@@ -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)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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()
|
|
||||||
|
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@ from qfluentwidgets import FluentIcon as FIF
|
|||||||
from qfluentwidgets import InfoBar, DisplayLabel
|
from qfluentwidgets import InfoBar, DisplayLabel
|
||||||
from PySide6.QtCore import Qt, QUrl
|
from PySide6.QtCore import Qt, QUrl
|
||||||
from PySide6.QtGui import QDesktopServices
|
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 ..common.config import cfg, HELP_URL, FEEDBACK_URL, AUTHOR, VERSION, YEAR, isWin11
|
||||||
from ..components.line_edit_card import LineEditSettingCard
|
from ..components.line_edit_card import LineEditSettingCard
|
||||||
@@ -307,23 +307,29 @@ class SettingInterface(ScrollArea):
|
|||||||
parent=self
|
parent=self
|
||||||
)
|
)
|
||||||
|
|
||||||
# def __onDownloadFolderCardClicked(self):
|
def __onFolderCardClicked(self, item, card):
|
||||||
# """ download folder card clicked slot """
|
""" download folder card clicked slot """
|
||||||
# folder = QFileDialog.getExistingDirectory(
|
folder = QFileDialog.getExistingDirectory(
|
||||||
# self, self.tr("Choose folder"), "./")
|
self, self.tr("Choose folder"), "./")
|
||||||
# if not folder or cfg.get(cfg.downloadFolder) == folder:
|
if not folder or cfg.get(item) == folder:
|
||||||
# return
|
return
|
||||||
|
|
||||||
# cfg.set(cfg.downloadFolder, folder)
|
cfg.set(item, folder)
|
||||||
# self.downloadFolderCard.setContent(folder)
|
card.setContent(folder)
|
||||||
|
|
||||||
def __connectSignalToSlot(self):
|
def __connectSignalToSlot(self):
|
||||||
""" connect signal to slot """
|
""" connect signal to slot """
|
||||||
cfg.appRestartSig.connect(self.__showRestartTooltip)
|
cfg.appRestartSig.connect(self.__showRestartTooltip)
|
||||||
|
# gamePath
|
||||||
# music in the pc
|
self.gamePathCard.clicked.connect(lambda: self.__onFolderCardClicked(cfg.gamePath, self.gamePathCard))
|
||||||
# self.downloadFolderCard.clicked.connect(
|
# backup
|
||||||
# self.__onDownloadFolderCardClicked)
|
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
|
# personalization
|
||||||
self.themeCard.optionChanged.connect(lambda ci: setTheme(cfg.get(ci)))
|
self.themeCard.optionChanged.connect(lambda ci: setTheme(cfg.get(ci)))
|
||||||
|
|||||||
26
modules/create_backup.py
Normal file
26
modules/create_backup.py
Normal file
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
107
modules/fc_kks.py
Normal file
107
modules/fc_kks.py
Normal file
@@ -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")
|
||||||
56
modules/install_chara.py
Normal file
56
modules/install_chara.py
Normal file
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
47
modules/remove_chara.py
Normal file
47
modules/remove_chara.py
Normal file
@@ -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]")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
70
script.py
70
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()
|
class Script:
|
||||||
request.process()
|
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()
|
||||||
|
|||||||
0
util/__init__.py
Normal file
0
util/__init__.py
Normal file
90
util/config.py
Normal file
90
util/config.py
Normal file
@@ -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)
|
||||||
165
util/file_manager.py
Normal file
165
util/file_manager.py
Normal file
@@ -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)
|
||||||
|
|
||||||
34
util/logger.py
Normal file
34
util/logger.py
Normal file
@@ -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}")
|
||||||
|
|
||||||
Reference in New Issue
Block a user