Files
CollectMangaInfo/MgrCalibreUI.py
2026-07-17 11:27:16 +09:00

596 lines
21 KiB
Python

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_())
"""