diff --git a/.gitignore b/.gitignore index fedc62b..fb60906 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ Temp/ __pycache__/ .venv/ .git/ +download_sample.json diff --git a/MgrCalibreUI.py b/MgrCalibreUI.py index e7d533d..bc6f6fc 100644 --- a/MgrCalibreUI.py +++ b/MgrCalibreUI.py @@ -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) @@ -127,6 +229,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) @@ -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, '폴더 선택', '') + if util.IsEmptyStr(folder_path): + return - 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) + btn = self.sender() + if btn is not None: + btn.setEnabled(False) - if True == os.path.exists(pathDest): - util.DbgOut(f"폴더 이동 완료: {pathItem} -> {pathDest}") + itemsToMove = list(self.m_ListIncompleteMangas) - except Exception as e: - strMsg = f"폴더 이동 중 오류 발생: {e}" - util.DbgOut(strMsg, True) - QMessageBox.critical(self, "Error", strMsg) + 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 on_btn_ChkDuplicate_clicked(self): - if len(self.m_DictDuplicate) == 0: - util.DbgOut("중복 검사할 데이터가 없습니다.", True) - return - - for nID, paths in self.m_DictDuplicate.items(): - if len(paths) <= 1: - continue - - # 첫번째 만화의 해시를 검사한다. - # -해시가 멀쩡하면 정상으로 판단, 2번째부터는 그냥 지운다. - # 만약 해시가 이상하면 해시가 멀쩡한 놈을 찾는다. - # -멀쩡한 놈을 찾으면 그걸 보존, 다른 나머지를 지운다. - # 해시가 전부 이상하면 일단 전부 딴데로 백업. - # - 해당하는 파일을 다운받거나, 중복되는 것 중에서 조합해서 복원. 그건 다른 데서 하자. + 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: - return + 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)