Update .gitignore, DataClass.py, and 19 more files...

오랜만에 서버 정리하고 커밋. 파일 위치를 정리했다.
캘리버 DB 를 열고 정보를 열람. Pupil 을 통해 다운받은 정보를 관리하기 위해 새로운 클래스 추가
This commit is contained in:
2025-08-01 14:57:40 +09:00
parent f1345e2770
commit d5f2d82bc9
18 changed files with 1266 additions and 1079 deletions

467
MgrCalibreUI.py Normal file
View File

@@ -0,0 +1,467 @@
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_ListIncomplMngas: list[str] = []
def __init__(self):
super().__init__()
self.loadINI()
self.initDB()
self.initUI()
#
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', "~/캘리버 서재", type=str)
if False == os.path.exists(self.pathDB):
os.makedirs(self.pathDB, exist_ok=True)
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())
#
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)
util.DbgOut(f"Loading Calibre DB from {pathCalibreDB}", True)
self.DBCalibre = MgrCalibreDB.MgrCalibreDB(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(self.pathLastCalibre)
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_ListNotCompleteMngas.append(pathDir)
self.SrcTableRowBgColor(nRow, Qt.GlobalColor.lightGray)
nRow += 1
del data
## metadata.db
def LoadCalibreDB(self, path:str ) -> None:
pathDB = os.path.join(path, "metadata.db")
if False == os.path.exists(pathDB):
util.DbgOut(f"Calibre DB not found: {pathDB}", True)
return
DB = MgrCalibreDB.MgrCalibreDB(pathDB)
listItems = DB.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 = DB.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_ListNotCompleteMngas.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_ListNotCompleteMngas:
# 유효하지 않은 경로면 건드리지 말자
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):
pass
#
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_())
"""