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 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 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) if False == os.path.exists(self.pathLog): os.makedirs(self.pathLog, exist_ok=True) self.pathDB = settings.value('PupilDBPath', "~/Workspace/DB", type=str) 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.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) 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.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 # 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 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") 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) for idx in range(itemCount): item = self.listWidget_Folders.item(idx) if item is None: continue self.LoadSrcFolder(item.text()) # 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_Emptyfolder_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) return for nID, paths in self.m_DictDuplicate.items(): if len(paths) <= 1: continue # 첫번째 만화의 해시를 검사한다. # -해시가 멀쩡하면 정상으로 판단, 2번째부터는 그냥 지운다. # 만약 해시가 이상하면 해시가 멀쩡한 놈을 찾는다. # -멀쩡한 놈을 찾으면 그걸 보존, 다른 나머지를 지운다. # 해시가 전부 이상하면 일단 전부 딴데로 백업. # - 해당하는 파일을 다운받거나, 중복되는 것 중에서 조합해서 복원. 그건 다른 데서 하자. # def on_btn_Archive_clicked(self): itemCount = self.tableWidget_Src.rowCount() if 0 >= itemCount: return for idx in range(itemCount): item = self.tableWidget_Src.item(idx, 0) self.LoadPupilJson(item.text()) # def on_btn_EnterCalibre_clicked(self): pass """ if __name__ == '__main__': app = QApplication(sys.argv) app.setQuitOnLastWindowClosed(True) main_window = MyApp() #main_window.show() sys.exit(app.exec_()) """