오래 걸리는 작업이 쓰레드로 돌아가게 수정, UI 다운 안되도록

This commit is contained in:
2026-07-17 11:26:25 +09:00
parent fccb8d7ab2
commit c00c592bd5
2 changed files with 275 additions and 122 deletions

1
.gitignore vendored
View File

@@ -4,3 +4,4 @@ Temp/
__pycache__/
.venv/
.git/
download_sample.json

View File

@@ -1,5 +1,6 @@
import sys
import os
import json
import shutil
import UtilPack as util
import DataClass_Pupil as pupil
@@ -8,7 +9,7 @@ import MgrPupilColDB
#import MgrCalibreLibs as calLib
from PyQt5.QtCore import Qt, QSettings, QRect
from PyQt5.QtCore import Qt, QSettings, QRect, QThread, QObject, pyqtSignal
from PyQt5.QtWidgets import QApplication, QWidget, QMainWindow, QPushButton, QVBoxLayout, QLineEdit, \
QHBoxLayout, QTableWidget, QTableWidgetItem, QAbstractItemView, QHeaderView, QFileDialog, \
QListWidget, QListWidgetItem, QMessageBox
@@ -16,6 +17,104 @@ from PyQt5.QtGui import QResizeEvent, QCloseEvent, QColor
#QApplication.setAttribute(Qt.AA_ShareOpenGLContexts)
class FolderParseWorker(QObject):
itemParsed = pyqtSignal(str, str, str, int, int, int)
finished = pyqtSignal()
def __init__(self, folderPaths: list[str]):
super().__init__()
self.folderPaths = folderPaths
def run(self):
for path in self.folderPaths:
listFiles = util.ListChildDirectories(path)
for item in listFiles:
pathDir = os.path.join(path, item)
if not os.path.isdir(pathDir):
continue
FullPath = os.path.join(pathDir, ".metadata")
data = pupil.PupilData(FullPath)
title = data.GetTitle()
strID = data.GetHitomiID()
if util.IsEmptyStr(strID):
strID = util.GetTextInBrakets(item)[0]
nImgListLen = data.GetImgFileCount()
nImgFileCnt = len(util.ListContainFiles(pathDir))
nID = int(strID)
self.itemParsed.emit(pathDir, title, strID, nImgListLen, nImgFileCnt, nID)
del data
self.finished.emit()
class ValidateWorker(QObject):
itemMoved = pyqtSignal(str, str)
error = pyqtSignal(str)
finished = pyqtSignal()
def __init__(self, folderPaths: list[str], destPath: str):
super().__init__()
self.folderPaths = folderPaths
self.destPath = destPath
def run(self):
for pathItem in self.folderPaths:
if not os.path.exists(pathItem):
continue
try:
baseName = os.path.basename(pathItem)
pathDest = os.path.join(self.destPath, baseName)
shutil.move(pathItem, pathDest)
if os.path.exists(pathDest):
self.itemMoved.emit(pathItem, pathDest)
except Exception as e:
strMsg = f"폴더 이동 중 오류 발생: {e}"
self.error.emit(strMsg)
self.finished.emit()
class ArchiveWorker(QObject):
itemArchived = pyqtSignal(str, str)
error = pyqtSignal(str)
finished = pyqtSignal()
def __init__(self, folderPaths: list[str], destPath: str):
super().__init__()
self.folderPaths = folderPaths
self.destPath = destPath
def run(self):
for src_folder in self.folderPaths:
baseName = os.path.basename(src_folder)
parent_dir = os.path.dirname(src_folder)
zip_path = os.path.join(self.destPath, f"{baseName}.zip")
try:
files = []
for root, dirs, files_in_dir in os.walk(src_folder):
for file in files_in_dir:
file_path = os.path.join(root, file)
files.append(file_path)
bSuccess = util.CreateZIP(zip_path, files)
if bSuccess:
self.itemArchived.emit(src_folder, zip_path)
else:
self.error.emit(f"압축 실패 ({baseName})")
except Exception as e:
self.error.emit(f"압축 중 오류 발생 ({baseName}): {e}")
self.finished.emit()
class MyApp(QMainWindow):
m_DictDuplicate: dict[int, list[str]] = {}
m_ListIncompleteMangas: list[str] = []
@@ -54,16 +153,19 @@ class MyApp(QMainWindow):
settings = QSettings('MyApp', 'settings')
self.pathLog = settings.value('LoggingPath', "~/Workspace/Log", type=str)
self.pathLog = os.path.expanduser(self.pathLog)
if False == os.path.exists(self.pathLog):
os.makedirs(self.pathLog, exist_ok=True)
self.pathDB = settings.value('PupilDBPath', "~/Workspace/DB", type=str)
self.pathDB = os.path.expanduser(self.pathDB)
if False == os.path.exists(self.pathDB):
os.makedirs(self.pathDB, exist_ok=True)
self.pathLastCalibre = settings.value('LastCalibrePath', "~/Calibre Library", type=str)
if False == os.path.exists(self.pathDB):
os.makedirs(self.pathDB, exist_ok=True)
self.pathLastCalibre = os.path.expanduser(self.pathLastCalibre)
if False == os.path.exists(self.pathLastCalibre):
os.makedirs(self.pathLastCalibre, exist_ok=True)
self.edit_DB.setText(self.pathLastCalibre)
@@ -128,6 +230,21 @@ class MyApp(QMainWindow):
layout_top.addWidget(self.listWidget_Folders)
layout_top.addLayout(layout_Btns)
layout_mid = QVBoxLayout()
btn_ChkDuplicate = QPushButton("유효성 검사")
btn_ChkDuplicate.clicked.connect(self.on_btn_Validate_clicked)
btn_Archive = QPushButton("압축 및 데이터 저장")
btn_Archive.clicked.connect(self.on_btn_Archive_clicked)
btn_EnterCalibre = QPushButton("컬리버에 삽입")
btn_EnterCalibre.clicked.connect(self.on_btn_EnterCalibre_clicked)
btn_MakeDownList = QPushButton("다운로드 목록 생성")
btn_MakeDownList.clicked.connect(self.on_btn_MakeDownList_clicked)
layout_mid.addWidget(btn_ChkDuplicate)
layout_mid.addWidget(btn_Archive)
layout_mid.addWidget(btn_EnterCalibre)
layout_mid.addWidget(btn_MakeDownList)
self.tableWidget_Src = QTableWidget()
self.tableWidget_Src.verticalHeader().setVisible(False)
self.tableWidget_Src.setEditTriggers(QAbstractItemView.NoEditTriggers)
@@ -141,29 +258,14 @@ class MyApp(QMainWindow):
self.tableWidget_Src.itemSelectionChanged.connect(self.on_tableWidget_Src_itemSelectionChanged)
layout.addLayout(layout_top)
layout.addLayout(layout_mid)
layout.addWidget(self.tableWidget_Src)
return layout
#
def MakeUI_Center(self):
layout = QVBoxLayout()
btn_Emptyfolder = QPushButton("빈 폴더 이동")
btn_Emptyfolder.clicked.connect(self.on_btn_Emptyfolder_clicked)
btn_ChkDuplicate = QPushButton("중복 검사 및 제거")
btn_ChkDuplicate.clicked.connect(self.on_btn_ChkDuplicate_clicked)
btn_Archive = QPushButton("압축 및 데이터 저장")
btn_Archive.clicked.connect(self.on_btn_Archive_clicked)
btn_EnterCalibre = QPushButton("컬리버에 삽입")
btn_EnterCalibre.clicked.connect(self.on_btn_EnterCalibre_clicked)
layout.addWidget(btn_Emptyfolder)
layout.addWidget(btn_ChkDuplicate)
layout.addWidget(btn_Archive)
layout.addWidget(btn_EnterCalibre)
return layout
pass
#
def MakeUI_Right(self):
@@ -226,71 +328,6 @@ class MyApp(QMainWindow):
if item:
item.setBackground(Qt.GlobalColor(color))
#
def LoadSrcFolder(self, path:str)-> None:
# 폴더가 없으면 리턴
if True == util.IsEmptyStr(path) or False == os.path.exists(path):
return
# 폴더의 내용물을 읽어온다.
listFiles = util.ListChildDirectories(path)
util.DbgOut(f"Src Count : {len(listFiles)}", True)
# 테이블 초기화
nLastRow = self.tableWidget_Src.rowCount()
self.tableWidget_Src.setRowCount(nLastRow + len(listFiles))
nRow = nLastRow
# 테이블에 데이터 추가
for item in listFiles:
pathDir = os.path.join(path, item)
if False == os.path.isdir(pathDir):
util.DbgOut(f"Not a directory: {pathDir}", True)
continue
item_0 = QTableWidgetItem(pathDir)
self.tableWidget_Src.setItem(nRow, 0, item_0)
FullPath = os.path.join(pathDir, ".metadata")
data = pupil.PupilData(FullPath)
util.DbgOut(f"Loaded MetaData Path : {FullPath}", True)
item_1 = QTableWidgetItem(data.GetTitle())
self.tableWidget_Src.setItem(nRow, 1, item_1)
strID = data.GetHitomiID()
if True == util.IsEmptyStr(strID):
strID = util.GetTextInBrakets(item)[0]
item_2 = QTableWidgetItem(strID)
self.tableWidget_Src.setItem(nRow, 2, item_2)
nImgListLen = data.GetImgFileCount()
item_3 = QTableWidgetItem(f"{nImgListLen}")
self.tableWidget_Src.setItem(nRow, 3, item_3)
nImgFileCnt = len(util.ListContainFiles(pathDir))
item_4 = QTableWidgetItem(f"{nImgFileCnt}")
self.tableWidget_Src.setItem(nRow, 4, item_4)
# 중복 검사 를 위해 경로를 딕셔너리에 저장
nID = int(strID)
if nID not in self.m_DictDuplicate:
self.m_DictDuplicate[nID] = []
self.m_DictDuplicate[nID].append(pathDir)
# 중복된 만화가 있으면 배경색을 변경
if len(self.m_DictDuplicate[nID]) > 1 :
self.SrcTableRowBgColor(nRow, Qt.GlobalColor.green)
# JSon 데이터의 파일 개수와 실제 다운받은 파일 개수가 다르면 리스트에 저장, 표시한다.
if 0 >= nImgFileCnt or 0 >= nImgListLen or nImgFileCnt != nImgListLen:
self.m_ListIncompleteMangas.append(pathDir)
self.SrcTableRowBgColor(nRow, Qt.GlobalColor.lightGray)
nRow += 1
del data
#
def LoadPupilJson(self, path:str) -> None:
itemPathFull = os.path.join(path, ".metadata")
@@ -379,12 +416,53 @@ class MyApp(QMainWindow):
# 테이블 위젯 비우기
self.tableWidget_Src.setRowCount(0)
folderPaths = []
for idx in range(itemCount):
item = self.listWidget_Folders.item(idx)
if item is None:
continue
folderPaths.append(item.text())
self.LoadSrcFolder(item.text())
btn = self.sender()
if btn is not None:
btn.setEnabled(False)
self._thread = QThread()
self._worker = FolderParseWorker(folderPaths)
self._worker.moveToThread(self._thread)
self._thread.started.connect(self._worker.run)
self._worker.itemParsed.connect(self._onItemParsed)
self._worker.finished.connect(self._thread.quit)
self._worker.finished.connect(self._worker.deleteLater)
self._thread.finished.connect(self._thread.deleteLater)
if btn is not None:
self._thread.finished.connect(lambda: btn.setEnabled(True))
self._thread.start()
#
def _onItemParsed(self, pathDir: str, title: str, strID: str,
nImgListLen: int, nImgFileCnt: int, nID: int):
nRow = self.tableWidget_Src.rowCount()
self.tableWidget_Src.setRowCount(nRow + 1)
self.tableWidget_Src.setItem(nRow, 0, QTableWidgetItem(pathDir))
self.tableWidget_Src.setItem(nRow, 1, QTableWidgetItem(title))
self.tableWidget_Src.setItem(nRow, 2, QTableWidgetItem(strID))
self.tableWidget_Src.setItem(nRow, 3, QTableWidgetItem(f"{nImgListLen}"))
self.tableWidget_Src.setItem(nRow, 4, QTableWidgetItem(f"{nImgFileCnt}"))
if nID not in self.m_DictDuplicate:
self.m_DictDuplicate[nID] = []
self.m_DictDuplicate[nID].append(pathDir)
if len(self.m_DictDuplicate[nID]) > 1:
self.SrcTableRowBgColor(nRow, Qt.GlobalColor.green)
if nImgFileCnt <= 0 or nImgListLen <= 0 or nImgFileCnt != nImgListLen:
self.m_ListIncompleteMangas.append(pathDir)
self.SrcTableRowBgColor(nRow, Qt.GlobalColor.lightGray)
#
def on_btnDB_clicked(self):
@@ -423,61 +501,135 @@ class MyApp(QMainWindow):
pass
#
def on_btn_Emptyfolder_clicked(self):
def on_btn_Validate_clicked(self):
folder_path = QFileDialog.getExistingDirectory(self, '폴더 선택', '')
for pathItem in self.m_ListIncompleteMangas:
# 유효하지 않은 경로면 건드리지 말자
if False == os.path.exists(pathItem):
continue
# 폴더 이동
try:
baseName = os.path.basename(pathItem)
pathDest = os.path.join(folder_path, baseName)
shutil.move(pathItem, pathDest)
if True == os.path.exists(pathDest):
util.DbgOut(f"폴더 이동 완료: {pathItem} -> {pathDest}")
except Exception as e:
strMsg = f"폴더 이동 중 오류 발생: {e}"
util.DbgOut(strMsg, True)
QMessageBox.critical(self, "Error", strMsg)
#
def on_btn_ChkDuplicate_clicked(self):
if len(self.m_DictDuplicate) == 0:
util.DbgOut("중복 검사할 데이터가 없습니다.", True)
if util.IsEmptyStr(folder_path):
return
for nID, paths in self.m_DictDuplicate.items():
if len(paths) <= 1:
continue
btn = self.sender()
if btn is not None:
btn.setEnabled(False)
# 첫번째 만화의 해시를 검사한다.
# -해시가 멀쩡하면 정상으로 판단, 2번째부터는 그냥 지운다.
# 만약 해시가 이상하면 해시가 멀쩡한 놈을 찾는다.
# -멀쩡한 놈을 찾으면 그걸 보존, 다른 나머지를 지운다.
# 해시가 전부 이상하면 일단 전부 딴데로 백업.
# - 해당하는 파일을 다운받거나, 중복되는 것 중에서 조합해서 복원. 그건 다른 데서 하자.
itemsToMove = list(self.m_ListIncompleteMangas)
self._threadValidate = QThread()
self._workerValidate = ValidateWorker(itemsToMove, folder_path)
self._workerValidate.moveToThread(self._threadValidate)
self._threadValidate.started.connect(self._workerValidate.run)
self._workerValidate.itemMoved.connect(self._onValidateItemMoved)
self._workerValidate.error.connect(self._onValidateError)
self._workerValidate.finished.connect(self._threadValidate.quit)
self._workerValidate.finished.connect(self._workerValidate.deleteLater)
self._threadValidate.finished.connect(self._threadValidate.deleteLater)
if btn is not None:
self._threadValidate.finished.connect(lambda: btn.setEnabled(True))
self._threadValidate.start()
#
def _onValidateItemMoved(self, pathItem: str, pathDest: str):
util.DbgOut(f"폴더 이동 완료: {pathItem} -> {pathDest}")
#
def _onValidateError(self, strMsg: str):
util.DbgOut(strMsg, True)
QMessageBox.critical(self, "Error", strMsg)
#
def on_btn_Archive_clicked(self):
itemCount = self.tableWidget_Src.rowCount()
if 0 >= itemCount:
if itemCount <= 0:
return
folder_path = QFileDialog.getExistingDirectory(self, '압축 파일 저장 폴더 선택', '')
if util.IsEmptyStr(folder_path):
return
btn = self.sender()
if btn is not None:
btn.setEnabled(False)
folderPaths = []
for idx in range(itemCount):
item = self.tableWidget_Src.item(idx, 0)
self.LoadPupilJson(item.text())
if item is not None:
folderPaths.append(item.text())
self._threadArchive = QThread()
self._workerArchive = ArchiveWorker(folderPaths, folder_path)
self._workerArchive.moveToThread(self._threadArchive)
self._threadArchive.started.connect(self._workerArchive.run)
self._workerArchive.itemArchived.connect(self._onArchiveItemArchived)
self._workerArchive.error.connect(self._onArchiveError)
self._workerArchive.finished.connect(self._threadArchive.quit)
self._workerArchive.finished.connect(self._workerArchive.deleteLater)
self._threadArchive.finished.connect(self._threadArchive.deleteLater)
if btn is not None:
self._threadArchive.finished.connect(lambda: btn.setEnabled(True))
self._threadArchive.start()
#
def _onArchiveItemArchived(self, src_folder: str, zip_path: str):
util.DbgOut(f"압축 완료: {src_folder} -> {zip_path}")
#
def _onArchiveError(self, strMsg: str):
util.DbgOut(strMsg, True)
QMessageBox.critical(self, "Error", strMsg)
#
def on_btn_EnterCalibre_clicked(self):
pass
"""
#
def on_btn_MakeDownList_clicked(self):
folder_path = QFileDialog.getExistingDirectory(self, '폴더 선택', '')
if util.IsEmptyStr(folder_path):
return
result = {}
listDirs = util.ListChildDirectories(folder_path)
for item in listDirs:
pathDir = os.path.join(folder_path, item)
if not os.path.isdir(pathDir):
continue
metadataPath = os.path.join(pathDir, ".metadata")
if not os.path.exists(metadataPath):
continue
data = pupil.PupilData(metadataPath)
if data.m_data is None:
continue
title = data.GetTitle()
strID = data.GetHitomiID()
if util.IsEmptyStr(strID):
strID = util.GetTextInBrakets(item)[0]
nImgListLen = data.GetImgFileCount()
nImgFileCnt = len(util.ListContainFiles(pathDir))
if nImgFileCnt <= 0 or nImgListLen <= 0 or nImgFileCnt != nImgListLen:
if not util.IsEmptyStr(strID):
result[strID] = title
if result:
timestamp = util.GetCurrentTime()
filename = f"download_{timestamp}.json"
savePath = os.path.join(folder_path, filename)
with open(savePath, "w", encoding="utf-8") as f:
json.dump(result, f, ensure_ascii=False, indent=2)
util.DbgOut(f"다운로드 목록이 생성되었습니다: {savePath}", True)
else:
util.DbgOut("완료되지 않은 다운로드가 없습니다.", True)
"""
if __name__ == '__main__':
app = QApplication(sys.argv)