import sys import os import shutil import UtilPack as util import DataClass_Pupil as pupil import MgrCalibreDB import MgrPupilColDB #import MgrCalibreLibs as calLib 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 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] = [] def __init__(self): super().__init__() self.initUI() self.loadINI() self.initDB() # def closeEvent(self, a0: 'QCloseEvent | None'): filenameLog = f"{util.GetCurrentTime()}_DbgLog.txt" pathDbgLog = os.path.join( self.pathLog, filenameLog) util.SaveDbgMessages(pathDbgLog) self.saveINI() super().closeEvent(a0) # def resizeEvent(self, a0: 'QResizeEvent | None'): PScreen = QApplication.primaryScreen() rcDesktop: QRect = QRect(0, 0, 800, 600) if PScreen is not None: rcDesktop = PScreen.availableGeometry() rcWnd = self.geometry() if rcWnd.width() > rcDesktop.width(): rcWnd.setWidth(rcDesktop.width()) print(f"Resize Event: {rcWnd.width()}x{rcWnd.height()} at {rcWnd.x()},{rcWnd.y()}") super().resizeEvent(a0) # def loadINI(self): 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) 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) self.fileNamePupilDB = "MyMangaData.db" self.fileNameCallibreDB = "metadata.db" window_size = settings.value('window/size', '800, 600') window_position = settings.value('window/position', '100, 100') ItemCnt = settings.value('folders/count', 0, type=int) for idx in range(ItemCnt): item_text = settings.value(f'folders/{idx}', '', type=str) if item_text: self.listWidget_Folders.addItem(item_text) # def saveINI(self): settings = QSettings('MyApp', 'settings') # 키-값 형식으로 데이터 저장 settings.setValue('window/size', "1024,768") settings.setValue('window/position', "100,100") # 폴더 목록 저장 ListItems: list[QListWidgetItem] = self.listWidget_Folders.items(None) for idx in range(len(ListItems)): item = ListItems[idx] settings.setValue(f'folders/{idx}', item.text()) settings.setValue('folders/count', self.listWidget_Folders.count()) settings.setValue('LastCalibrePath', self.edit_DB.text()) # def initDB(self): pathPupilDB = os.path.join(self.pathDB, self.fileNamePupilDB) util.DbgOut(f"Loading Pupil DB from {pathPupilDB}", True) self.DBPupil = MgrPupilColDB.MgrPupilColDB(pathPupilDB) pathCalibreDB = os.path.join(self.pathLastCalibre, self.fileNameCallibreDB) self.LoadCalibreDB(pathCalibreDB) # def MakeUI_Left(self): layout = QVBoxLayout() layout_top = QHBoxLayout() self.listWidget_Folders = QListWidget() self.listWidget_Folders.setFixedHeight(100) layout_Btns = QVBoxLayout() btn_FolderAdd = QPushButton("폴더 추가") btn_FolderAdd.clicked.connect(self.on_btnFolderAdd_clicked) btn_FolderDel = QPushButton("폴더 삭제") btn_FolderDel.clicked.connect(self.on_btnFolderDel_clicked) btn_FolderParss = QPushButton("폴더 파싱") btn_FolderParss.clicked.connect(self.on_btnFolderParse_clicked) layout_Btns.addWidget(btn_FolderAdd) layout_Btns.addWidget(btn_FolderDel) layout_Btns.addWidget(btn_FolderParss) 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(".Download 리스트 작성") btn_EnterCalibre.clicked.connect(self.on_btn_MakeDownList_clicked) layout_mid.addWidget(btn_ChkDuplicate) layout_mid.addWidget(btn_Archive) layout_mid.addWidget(btn_EnterCalibre) self.tableWidget_Src = QTableWidget() self.tableWidget_Src.verticalHeader().setVisible(False) self.tableWidget_Src.setEditTriggers(QAbstractItemView.NoEditTriggers) self.tableWidget_Src.setSortingEnabled(True) self.tableWidget_Src.setColumnCount(5) self.tableWidget_Src.setHorizontalHeaderLabels(["Path", "Title", "H.ID", "Img Cnt", "Dw Cnt"]) self.tableWidget_Src.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) self.tableWidget_Src.horizontalHeader().sectionClicked.connect(self.on_tableWidget_Src_headerClicked) self.tableWidget_Src.setSelectionBehavior(QAbstractItemView.SelectRows) self.tableWidget_Src.setSelectionMode(QAbstractItemView.SingleSelection) 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): pass # def MakeUI_Right(self): layout_top = QHBoxLayout() self.edit_DB = QLineEdit(self) self.edit_DB.setReadOnly(True) self.edit_DB.setText("...") btn_DB = QPushButton("...") btn_DB.setFixedWidth(50) btn_DB.clicked.connect(self.on_btnDB_clicked) layout_top.addWidget(self.edit_DB) layout_top.addWidget(btn_DB) self.tableWidget_DB = QTableWidget() self.tableWidget_DB.verticalHeader().setVisible(False) self.tableWidget_DB.setColumnCount(5) self.tableWidget_DB.setHorizontalHeaderLabels(["Title", "Author", "Cover", "Exts", "ID"]) self.tableWidget_DB.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) self.tableWidget_DB.setSelectionBehavior(QAbstractItemView.SelectRows) self.tableWidget_DB.setSelectionMode(QAbstractItemView.SingleSelection) self.tableWidget_DB.itemSelectionChanged.connect(self.on_tableWidget_DB_itemSelectionChanged) layout = QVBoxLayout() layout.addLayout(layout_top) layout.addWidget(self.tableWidget_DB) return layout # def MakeUI(self): layout_L = self.MakeUI_Left() layout_C = self.MakeUI_Center() layout_R = self.MakeUI_Right() # 레이아웃 설정 layout = QHBoxLayout() layout.addLayout(layout_L, stretch = 10) layout.addLayout(layout_C, stretch = 1) layout.addLayout(layout_R, stretch = 10) return layout # def initUI(self): layout = self.MakeUI() # 레이아웃을 윈도우에 적용 central_widget = QWidget() central_widget.setLayout(layout) self.setCentralWidget(central_widget) self.setWindowTitle('Manga Database') self.resize(800, 600) self.show() # 테이블의 특정 행에 배경색을 설정한다 # nRow: 배경색을 설정할 행 번호, color: 배경색 (Qt.GlobalColor) def SrcTableRowBgColor(self, nRow:int, color:Qt.GlobalColor) -> None: for col in range(self.tableWidget_Src.columnCount()): item = self.tableWidget_Src.item(nRow, col) if item: item.setBackground(Qt.GlobalColor(color)) # def LoadPupilJson(self, path:str) -> None: itemPathFull = os.path.join(path, ".metadata") if False == os.path.exists(itemPathFull): util.DbgOut(f"JSon File not found: {itemPathFull}", True) return data = pupil.PupilData(itemPathFull) print(data.GetText()) ## metadata.db def LoadCalibreDB(self, path:str ) -> None: pathDB = path if True == os.path.isdir(path): pathDB = os.path.join(path, "metadata.db") print(f"LoadCalibreDB: {pathDB}") if False == os.path.exists(pathDB): util.DbgOut(f"Calibre DB not found: {pathDB}", True) return self.DBCalibre = MgrCalibreDB.MgrCalibreDB(pathDB) listItems = self.DBCalibre.GetBookListforUI_ArcList() for item in listItems: strID = item[4] strTitle = item[0] strAuthor = item[1] strHasCover = item[2] strBookPath = item[3] nRow = self.tableWidget_DB.rowCount() self.tableWidget_DB.setRowCount(nRow + 1) self.tableWidget_DB.setItem(nRow, 0, QTableWidgetItem(strTitle)) self.tableWidget_DB.setItem(nRow, 1, QTableWidgetItem(strAuthor)) itemCover = QTableWidgetItem(strHasCover) if "0" == strHasCover: itemCover.setForeground(QColor(255, 0, 0)) self.tableWidget_DB.setItem(nRow, 2, itemCover) # format, uncompressed_size, name tupleData = self.DBCalibre.GetDataByBookID(int(strID)) itemExt = QTableWidgetItem(tupleData[0]) pathArc = os.path.join(path, strBookPath, f"{tupleData[2]}.{tupleData[0]}") if False == os.path.exists(pathArc): itemExt.setForeground(QColor(255, 0, 0)) self.tableWidget_DB.setItem(nRow, 3, itemExt) self.tableWidget_DB.setItem(nRow, 4, QTableWidgetItem(strID)) # def on_btnFolderAdd_clicked(self): folder_path = QFileDialog.getExistingDirectory(self, '폴더 선택', '') if True == util.IsEmptyStr(folder_path): return pathAbs = os.path.abspath(folder_path) if self.listWidget_Folders.findItems(pathAbs, Qt.MatchFlag.MatchExactly): util.DbgOut(f"폴더가 이미 추가되어 있습니다: {pathAbs}", True) return self.listWidget_Folders.addItem(pathAbs) # def on_btnFolderDel_clicked(self): selected_items = self.listWidget_Folders.selectedItems() for item in selected_items: row = self.listWidget_Folders.row(item) self.listWidget_Folders.takeItem(row) # def on_btnFolderParse_clicked(self): itemCount = self.listWidget_Folders.count() if 0 == itemCount: util.DbgOut("폴더가 선택되지 않았습니다.", True) return # 중복 검사을 위한 딕셔너리 초기화 self.m_DictDuplicate.clear() # 다운로드가 완료되지 않은 만화 목록 초기화 self.m_ListIncompleteMangas.clear() # 테이블 위젯 비우기 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()) 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): folder_path = QFileDialog.getExistingDirectory(self, '폴더 선택', '') if True == util.IsEmptyStr(folder_path): return if self.edit_DB.text() == folder_path: return self.edit_DB.setText(folder_path) self.LoadCalibreDB(folder_path) # def on_tableWidget_Src_headerClicked(self, logicalIndex: int): # 정렬 상태 확인 및 변경 HorHeader = self.tableWidget_Src.horizontalHeader() if HorHeader is None: return if HorHeader.sortIndicatorOrder() == Qt.SortOrder.AscendingOrder: sort_order = Qt.SortOrder.DescendingOrder else: sort_order = Qt.SortOrder.AscendingOrder HorHeader.setSortIndicator(logicalIndex, sort_order) self.tableWidget_Src.sortItems(logicalIndex, HorHeader.sortIndicatorOrder() ) # def on_tableWidget_Src_itemSelectionChanged(self): pass # def on_tableWidget_DB_itemSelectionChanged(self): pass # def on_btn_Validate_clicked(self): folder_path = QFileDialog.getExistingDirectory(self, '폴더 선택', '') if util.IsEmptyStr(folder_path): return btn = self.sender() if btn is not None: btn.setEnabled(False) 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 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) 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_MakeDownList_clicked(self): pass """ if __name__ == '__main__': app = QApplication(sys.argv) app.setQuitOnLastWindowClosed(True) main_window = MyApp() #main_window.show() sys.exit(app.exec_()) """