Compare commits

..

10 Commits

Author SHA1 Message Date
Young Hoon Lee
061ee37ba2 dkdkdkdkdkdk 2025-09-23 19:46:03 +09:00
9d98b8d19b Update DataClass_Pupil.py, MgrCalibreUI.py, and 2 more files... 2025-09-07 23:44:30 +09:00
2c36327ae8 Update MgrCalibreDB.py, MgrCalibreUI.py, and 3 more files...
python - requirements.txt added
2025-08-03 21:03:16 +09:00
d5f2d82bc9 Update .gitignore, DataClass.py, and 19 more files...
오랜만에 서버 정리하고 커밋. 파일 위치를 정리했다.
캘리버 DB 를 열고 정보를 열람. Pupil 을 통해 다운받은 정보를 관리하기 위해 새로운 클래스 추가
2025-08-01 14:57:40 +09:00
f1345e2770 일단 정리.. 2025-07-15 11:14:05 +09:00
5d5c0ff675 Update GetArc_Hitomi.py 2025-01-28 17:45:19 +09:00
07ad09bb50 Update DataClass.py, GetArc_Hitomi.py, and 2 more files...
작업한거 일단 커밋. 딱히 기능을 추가한건 없지만, 정리는 조금 했다. 정리 더 해야 하는데, 윈도우에서 작업하려고....커밋!!
2025-01-28 17:45:03 +09:00
6fe1cf8da0 Update MgrCalibreDB.py, UI.py, and 4 more files... 2025-01-07 02:41:54 +09:00
7587da53b3 Update UI.py, UtilPack.py, and ui_Tki.py 2024-12-11 10:48:54 +09:00
1c840a1e25 Update MgrCalibreDB.py and UI.py 2024-12-08 01:06:45 +09:00
17 changed files with 1569 additions and 901 deletions

7
.gitignore vendored
View File

@@ -1,5 +1,4 @@
venv_mangainfo/
mangaDB.xlsx
temp.db
temp.json
untitled0.py
Data/
Temp/
__pycache__/

View File

@@ -1,49 +1,102 @@
class CBZInfo:
def __init__(self, title, url):
self.title = title
self.url = url
self.serires = ""
self.type = ""
self.filename = ""
self.torrent = ""
self.language = ""
self.gallery_id = 0
# 중복을 허용하지 않는 집합으로 초기화
self.related_galID = set()
self.artists = set()
self.tags = set()
def __str__(self):
return f"{self.title} by {self.author} ({self.publication_year})"
def AddTag(self, name):
self.tags.add(name)
def RmvTag(self, name):
self.tags.discard(name)
def AddArtist(self, name):
self.artists.add(name)
def RmvArtist(self, name):
self.artists.discard(name)
from typing import Set
import UtilPack as util
class TagInfo:
def __init__(self, name, url):
def __init__(self, name: str, url: str):
self.name = name
self.url = url
def __str__(self):
return f"{self.name} : {self.url}"
return f"#{self.name} : {self.url}"
class ImageFileInfo:
def __init__(self, name, height, width, hashValue, bWebp):
self.name = name
def __init__(self, path: str, height: int, width: int, hashValue: str, bWebp: bool):
self.path = path
self.height = height
self.width = width
self.hashValue = hashValue
self.bWebp = bWebp
def CehckHash(self) -> bool:
return util.VerifyIsValidFile(self.path, self.hashValue)
class CBZInfo:
def __init__(self, title: str, url: str):
self.title = title
self.url = url
self.series = ""
self.type = ""
self.Arcfilename = ""
self.torrent = ""
self.language = ""
self.gallery_id = 0
# 중복을 허용하지 않는 집합으로 초기화
self.related_galID = set[str]()
self.artists = set[str]()
self.tags = set[str]()
self.files = list[ImageFileInfo]()
def __str__(self):
strArtists = ", ".join(self.artists)
strTags = ", ".join(self.tags)
return f"ID : {self.gallery_id} - {self.title} by {strArtists} - #{strTags}"
def SaveToText(self):
retText = "{"
retText += f"Title : {self.title}\r\n"
retText += f"URL : {self.url}\r\n"
retText += f"Series : {self.series}\r\n"
retText += f"Type : {self.type}\r\n"
retText += f"Filename : {self.Arcfilename}\r\n"
retText += f"Torrent : {self.torrent}\r\n"
retText += f"Language : {self.language}\r\n"
retText += f"Gallery ID : {self.gallery_id}\r\n"
retText += self.ArtistsSaveToText() + f"\r\n"
retText += self.RelatedGalleryIDsSaveToText() + f"\r\n"
retText += self.TagsSaveToText() + f"\r\n"
retText += "}"
def TagListSaveToText(self, setTags: Set[str])-> str:
RetText = "{"
for tag in setTags:
RetText += (f"\"{tag}\" ")
RetText += "}"
return RetText
def ArtistsSaveToText(self)-> str:
retText = f"Artists : "
retText += self.TagListSaveToText(self.artists)
return retText
def RelatedGalleryIDsSaveToText(self)-> str:
retText = f"Gallery_ID : "
retText += self.TagListSaveToText(self.related_galID)
return retText
def TagsSaveToText(self)-> str:
retText = f"Tags : "
retText += self.TagListSaveToText(self.tags)
return retText
def AddTag(self, strTag: str):
self.tags.add(strTag)
def RmvTag(self, strTag: str):
self.tags.discard(strTag)
def AddArtist(self, strName: str):
self.artists.add(strName)
def RmvArtist(self, strName: str):
self.artists.discard(strName)

190
DataClass_Pupil.py Normal file
View File

@@ -0,0 +1,190 @@
import json
import os
import UtilPack as util
import DataClass as info
GALBLOCK = "galleryBlock"
GALURL = "galleryUrl"
GALINFO = "galleryInfo"
GALTAGS = "relatedTags"
JTITLE = "japanese_title"
# Example
#with open('test.db', 'r') as file:
# data = json.load(file)
#print_json_tree(data)
#print(data['galleryInfo']['tags'])
class PupilData:
m_data = None
def __init__(self, argv):
self.argv = argv
self.PupilJSONOpen(argv)
def __enter__(self):
pass
def __exit__(self, ex_type, ex_value, traceback):
pass
def PupilJSONOpen(self, path:str) -> None:
self.m_data = None
if True == util.IsEmptyStr(path):
util.DbgOut("PupilData: input Null", True)
return
if False == os.path.exists(path):
return
try:
with open(path, "r", encoding="utf-8") as file:
self.m_data = json.load(file)
except FileNotFoundError:
util.DbgOut(f"파일이 존재하지 않음: {path}", True)
except json.JSONDecodeError:
util.DbgOut(f"JSON 형식이 잘못됨: {path}", True)
except Exception as e:
util.DbgOut(f"기타 오류 발생: {e}", True)
# pupil 의 JSON 을 파싱해서 DataClass 에 데이터를 넣어 반환한다.
"""
def GetGalleryBlock(self):
['galleryBlock']
['id']
["galleryUrl"]
["thumbnails"]
["title"]
["artists"]
["series"]
["type"]
["language"]
["relatedTags"]
["tag"]
["url"]
["female"]
["male"]
["groups"]
def GetGalleryInfo(self):
["galleryInfo"]
["id"]
["title"]
["language"]
["type"]
["date"]
["artists"]
["artist"]
["url"]
["groups"]
["group"]
["url"]
["parodys"]
["parody"]
["url"]
["tags"]
["tag"]
["url"]
["female"]
["male"]
["related"]
["languages"]
["galleryid"]
["url"]
["language_localname"]
["name"]
["characters"]
["character"]
["url"]
["files"]
["width"]
["hash"]
["name"]
["height"]
["hasavif"]
def GetInfo(self):
if None == self.m_data:
return
title = self.m_data[GALINFO]["title"]
url = self.m_data[GALBLOCK]["galleryUrl"]
retInfo = info(title, url)
retInfo.type = self.m_data[GALINFO]["type"]
retInfo.language = self.m_data[GALINFO]["language"]
retInfo.gallery_id = self.m_data[GALINFO]["id"]
listArtists = self.m_data[GALINFO]["artists"]
for item in listArtists:
strArtist = item["artist"]
strUrl = item["url"]
strTag = f"artist:{strArtist}"
tempInfo = util.TagInfo(strTag, strUrl)
retInfo.AddArtist(tempInfo)
listTags = self.m_data[GALINFO]["tags"]
for item in listTags:
strGend = ""
if 1 == item["female"]:
strGend = "female:"
elif 1 == item["male"]:
strGend = "male:"
strTag = item["tag"]
strRelatedTag = f"{strGend}:{strTag}"
tagUrl = item[url]
tempInfo = util.TagInfo(strRelatedTag, tagUrl)
retInfo.AddTag(tempInfo)
return retInfo
"""
#
def GetTitle(self) -> str:
retStr : str = ""
if None != self.m_data:
retStr = self.m_data[GALINFO]["title"]
return retStr
#
def GetHitomiID(self) -> str:
retStr : str = ""
if None != self.m_data:
retStr = self.m_data[GALINFO]["id"]
return retStr
#
def GetImgFileCount(self) -> int:
if None == self.m_data:
return 0
return len(self.m_data[GALINFO]["files"])
# pupil 의 JSON 을 파싱해서 ImageFileList 를 반환한다.
def GetImgFileList(self) -> list[info.ImageFileInfo]:
listRet = set(info.ImageFileInfo)
if None != self.m_data:
listFiles = self.m_data[GALINFO]["files"]
for item in listFiles:
tempInfo = info.ImageFileInfo(item["name"],
item["height"],
item["width"],
item["hash"],
item["haswebp"])
listRet.append(tempInfo)
return listRet
#
def GetText(self) -> str:
return util.PrintJSONTree(self.m_data)

View File

@@ -5,105 +5,158 @@ from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException
from bs4 import BeautifulSoup
import time
import UtilPack as util
import DataClass as info
#
def GetSearchResult(searchWord):
url = getSiteUrl(searchWord)
class GetArc_Hitomi:
m_strBaseURL = "https://hitomi.la/"
util.DbgOut("Hitomi : " + url)
m_listTagsTemp = list[info.TagInfo]()
def __init__(self):
pass
def GetSearchResult(self, strWord: str, bSaveHTML: bool = False):
if util.IsEmptyStr(strWord):
util.DbgOut("Error : SearchWord is empty", True)
return
strURL = ""
if strWord.isdigit():
strURL = self.getSiteUrlForGallery(int(strWord))
else:
strURL = self.getSiteUrlForSearch(strWord)
util.DbgOut(f"Hitomi : {strURL}", True)
driver = webdriver.Chrome()
driver.get(url)
driver.get(strURL)
# 웹페이지가 로드될 때까지 기다리기
try:
WebDriverWait(driver, 30).until(
EC.presence_of_element_located((By.CLASS_NAME, 'lillie'))
WebDriverWait(driver, 10).until(
#EC.presence_of_element_located((By.CLASS_NAME, 'lillie'))
lambda d: d.execute_script("return document.readyState") == "complete"
)
except TimeoutException:
util.DbgOut("페이지가 로드되지 않았거나 요소를 찾을 수 없습니다.")
util.DbgOut("페이지가 로드되지 않았거나 요소를 찾을 수 없습니다.", True)
driver.quit()
return
strContent = driver.page_source
driver.quit()
parseMangaInfos(strContent)
if True == bSaveHTML:
strFileName = f"{strWord}_result.html"
with open(strFileName, "w", encoding="utf-8") as file:
file.write(strContent)
pass
util.DbgOut(f"HTML content saved to {strFileName}", True)
listRet = self.parseMangaInfos(strContent)
for Idx in range(len(listRet)):
util.DbgOut(f"{Idx} : {listRet[Idx]}", True)
#
def getSiteUrl(searchWord):
strRet = "https://hitomi.la/"
def GetListSearchResult(self, listID: list[int], bSave: bool = False):
driver = webdriver.Chrome()
if False == util.IsEmptyStr(searchWord):
if False == searchWord.isdigit():
strRet = strRet + "search.html?" + searchWord
else:
strRet = strRet + "galleries/" + searchWord + ".html"
# 웹페이지가 로드될 때까지 기다리기
try:
for nID in listID:
strURL = self.getSiteUrlForGallery(nID)
util.DbgOut(f"Hitomi : {strURL}", True)
return strRet
driver.get(strURL)
WebDriverWait(driver, 10).until(
lambda d: d.execute_script("return document.readyState") == "complete"
)
time.sleep(2)
strContent = driver.page_source
listRet = self.parseMangaInfos(strContent)
#for Idx in range(len(listRet)):
# print(f"{Idx} : {listRet[Idx]}")
try:
for Idx in range(len(listRet)):
print(f"{Idx} : {listRet[Idx]}")
with open( f"{id}.txt", 'w') as file:
for item in listRet[Idx]:
file.write( + "\n")
except IOError:
util.DbgOut(f"Error: Could not write to the file at {id}.txt.", True)
except Exception as e:
util.DbgOut(f"Hitomi Loading Error : {e}", True)
finally:
driver.quit()
def getSiteUrlForSearch(self, searchWord: str) -> str:
return f"{self.m_strBaseURL}search.html?{searchWord}"
def getSiteUrlForGallery(self, nHitomiID: int) -> str:
return f"{self.m_strBaseURL}galleries/{nHitomiID}.html"
#
def parseMangaInfos(html_doc):
def parseMangaInfos(self, html_doc : str) -> list[info.CBZInfo]:
# BeautifulSoup 객체 생성
soup = BeautifulSoup(html_doc, 'html.parser')
gallery_elements = soup.find_all(class_='gallery-content')
listDJs = []
listDJs: list[info.CBZInfo] = []
for element in gallery_elements:
listDJ = djParse(element)
listDJ = self.djParse(element)
listDJs.extend(listDJ)
print(len(listDJs))
return listDJs
def djParse(soup_element):
#
def djParse(self, soup_element) -> list[info.CBZInfo]:
childs = soup_element.find_all(class_='dj')
listInfos = []
listInfos: list[info.CBZInfo] = []
for child in childs:
info = djTitleParse(child)
listTag1 = djArtistParse(child, info)
listTag2 = djDescParse(child, info)
info = self.djTitleParse(child)
self.djArtistParse(child, info)
self.djDescParse(child, info)
listInfos.append(info)
return listInfos
def djTitleParse(input_element):
#
def djTitleParse(self, input_element):
element = input_element.find('h1', class_='lillie')
title = element.text
strTitle: str = element.text
a_tag = element.find('a')
url = a_tag.get('href')
strURL: str = a_tag.get('href')
util.DbgOut("title : " + title)
util.DbgOut("URl : " + url)
#util.DbgOut("title : " + title)
#util.DbgOut("URl : " + url)
return info.CBZInfo(title, url)
return info.CBZInfo(strTitle, strURL)
def djArtistParse(input_element, retPtr):
#
def djArtistParse(self, input_element, retPtr):
element = input_element.find('div', class_='artist-list')
a_tags = element.find_all('a')
listArtists = []
for tag in a_tags:
artist = tag.text
a_url = tag.get('href')
retPtr.AddArtist(artist)
listArtists.append( info.TagInfo(artist, a_url) )
return listArtists
def djDescParse(input_element, retPtr):
#
def djDescParse(self, input_element, retPtr):
element = input_element.find('table', class_='dj-desc')
tb_rows = element.find_all('tr')
listTags = []
@@ -113,7 +166,6 @@ def djDescParse(input_element, retPtr):
util.DbgOut("Warning : td get failed")
continue
outMsg = f"{tds[0].text} : \r\n"
a_tags = tds[1].find_all('a')
@@ -127,7 +179,7 @@ def djDescParse(input_element, retPtr):
outMsg += f" {tag_name} {tag_url}\r\n"
util.DbgOut(outMsg)
#util.DbgOut(outMsg)
#
if "Series" == tds[0]:
@@ -141,3 +193,22 @@ def djDescParse(input_element, retPtr):
return listTags
def main():
# Hitomi Search Test
hitomi = GetArc_Hitomi()
# 검색어로 검색
#hitomi.GetSearchResult("test")
# ID로 검색
hitomi.GetSearchResult("11107", True)
# ID 리스트로 검색
#listID = [1234567, 2345678, 3456789]
#hitomi.GetListSearchResult(listID, True)
# For Main Loop
if __name__ == '__main__':
main()

View File

@@ -1,18 +1,12 @@
import os
import sys
import sqlite3
import UtilPack as util
import MgrSQLiteDB as MyDB
class MgrCalibreDB:
def __init__(self, path):
self.path = path
def __enter__(self):
pass
def __exit__(self, ex_type, ex_value, traceback):
self.conn.close()
class MgrCalibreDB(MyDB.MgrSQLiteDB):
def __init__(self, path: str):
super().__init__(path)
self.init()
def init(self):
# 데이터베이스 연결 (파일이 없으면 새로 생성됨)
@@ -20,97 +14,179 @@ class MgrCalibreDB:
self.conn.create_function("title_sort", 1, self._title_sort )
self.cursor = self.conn.cursor()
#
def _title_sort(self, value):
return 0
def GetTableAll(self, table):
if None == self.cursor:
return None
if None == table or True == util.IsEmptyStr(table):
return None
# 테이블의 마지막 인덱스를 가져온다. 잘못되면 -1
def GetLastIndexID(self, strTableName: str) -> int :
if True == util.IsEmptyStr(strTableName):
return -1
strSQL = f"SELECT * FROM \"{table}\";"
self.cursor.execute(strSQL)
listRet = self.cursor.fetchall()
strSQL = f"SELECT MAX(id) FROM \'{strTableName}\';"
listMaxID = self.SQLExecute_Get(self.cursor, strSQL)
if listMaxID is None or 0 >= len(listMaxID):
util.DbgOut("Error : Invalid Table Name")
return -1
return listMaxID[0][0] + 1
#
def GetBookTitleByID(self, nID: int) -> str:
if 0 >= nID:
return ""
listRet = self.GetValuesOneCondition("books", "title", "id", str(nID))
if 0 >= len(listRet):
return ""
return listRet[0]
#
def GetAuthorByID(self, nID: int ) -> str:
if 0 >= nID:
return ""
listRet = self.GetValuesOneCondition("authors", "name", "id", f"{nID}")
if 0 >= len(listRet):
return ""
return listRet[0]
#
def GetAuthorsByBookID(self, nID: int ) -> list[str]:
if 0 >= nID:
return []
listAuthorID = self.GetValuesOneCondition("books_authors_link", "author", "book", f"{nID}")
print(f"GetAuthorsByBookID : {listAuthorID}")
if 0 >= len(listAuthorID):
return []
listRet = list[str]()
for strAuthorID in listAuthorID:
if True == util.IsEmptyStr(strAuthorID):
continue
nAuthorID = int(strAuthorID)
strAuthorName = self.GetAuthorByID(nAuthorID)
if True == util.IsEmptyStr(strAuthorName):
continue
listRet.append(strAuthorName)
return listRet
def GetTableSchm(self, table):
if None == self.cursor:
return None
#
def GetDataByBookID(self, nID: int ) -> tuple[str, int, str]:
if 0 >= nID:
return ("", 0, "")
if None == table or True == util.IsEmptyStr(table):
return None
strSQL = f"SELECT format, uncompressed_size, name FROM data WHERE book = {nID};"
listRet = self.SQLExecute_Get(self.cursor, strSQL)
if listRet is None or 0 >= len(listRet):
return ("", 0, "")
strSQL = f"PRAGMA table_info (\"{table}\");"
self.cursor.execute(strSQL)
listRet = self.cursor.fetchall()
return listRet[0]
#
def GetTagByID(self, strTag: str) -> int:
if True == util.IsEmptyStr(strTag):
return -1
listRet = self.GetValuesOneCondition("tags", "id", "name", strTag)
if 0 >= len(listRet):
return -1
return int(listRet[0])
# title, author, cover, ext(path), id
# id, title, has_cover, path, And author
def GetBookListforUI_ArcList(self) -> list[tuple[str, str, str, str, str]]:
strSQL = f"Select id, title, path, has_cover from books;"
listResult = self.SQLExecute_Get(self.cursor, strSQL)
if listResult is None or 0 >= len(listResult):
return []
listRet = list[tuple[str, str, str, str, str]]()
for item in listResult:
nID = item[0]
strTitle = item[1]
strPath = item[2]
nHasCover = item[3]
if 0 >= nID:
continue
listAuthors = self.GetAuthorsByBookID(nID)
strAuthor = ", ".join(listAuthors) if listAuthors else "Unknown"
listRet.append((strTitle, strAuthor, str(nHasCover), strPath, str(nID)))
return listRet
def UpdateTableValue(self,Table, nID, ColName, Value):
if None == self.cursor:
return
if 0 >= nID or True == util.IsEmptyStr(Table) or True == util.IsEmptyStr(ColName):
return
strSQL = f"UPDATE {Table} SET \'{ColName}\' = \'{Value}\' WHERE id = {nID};"
self.cursor.execute(strSQL)
self.conn.commit()
def InsertAuthor(self, strAuthor):
if None == self.cursor:
return
#
def InsertAuthor(self, strAuthor: str):
if True == util.IsEmptyStr(strAuthor):
return
strTableName = "authors"
strSQL = f"SELECT MAX(id) FROM \'{strTableName}\';"
self.cursor.execute(strSQL)
nMaxID = self.cursor.fetchone()[0]
nMaxID += 1.
nMaxID = self.GetLastIndexID(strTableName)
if 0 >= nMaxID:
util.DbgOut("Error : Invalid Table Name")
return
strAuthorSort = strAuthor.replace(" ", ",")
strSQL = f"INSERT OR IGNORE INTO \'{strTableName}\' VALUES ({nMaxID}, \'{strAuthor}\',\'{strAuthorSort}\', \'\');"
self.cursor.execute(strSQL)
self.conn.commit()
self.SQLExecute(self.cursor, strSQL, True)
def InsertTag(self, tag):
if None == self.cursor:
return
if True == util.IsEmptyStr(tag):
#
def InsertTag(self, strTag: str):
if True == util.IsEmptyStr(strTag):
return
strTableName = "tags"
strSQL = f"SELECT MAX(id) FROM \'{strTableName}\';"
self.cursor.execute(strSQL)
nMaxID = self.cursor.fetchone()[0]
nMaxID += 1.
nMaxID = self.GetLastIndexID(strTableName)
if 0 >= nMaxID:
util.DbgOut("Error : Invalid Table Name")
return
strSQL = f"INSERT OR IGNORE INTO \'{strTableName}\' VALUES ({nMaxID}, \'{strAuthor}\', \'\');"
self.cursor.execute(strSQL)
self.conn.commit()
strSQL = f"INSERT OR IGNORE INTO \'{strTableName}\' VALUES ({nMaxID}, \'{strTag}\', \'\');"
self.SQLExecute(self.cursor, strSQL, True)
""" #Example usage
def main():
db = MgrCalibreDB("/Users/minarinari/캘리버 서재/metadata.db")
db.init()
#db.UpdateTableValue("books", 3, "title", "삭막")
db.InsertAuthor("Oyster")
db.InsertAuthor("Rustle")
db.InsertAuthor("ShindoL")
listValues = db.GetTableAll("authors")
listValues = db.GetTableAllData("authors")
print(listValues)
db.InsertTag("female:Bondage")
listValues2 = db.GetTableAllData("Tags")
print(listValues2)
# For Main Loop
if __name__ == '__main__':
main()
"""
#coments table
# 자체 id, book id, 코멘트...<- 여기다 json 을 넣자.
# id, book ,comment
# data table
# 자체 id, book id, 파일 포멧, cbz, 크기, 파일 이름
# id, book, format, uncompressed_size, name
# 테이블 생성
#cursor.execute('''CREATE TABLE IF NOT EXISTS users

View File

@@ -11,6 +11,7 @@ from PIL import Image
m_ImgExts = [".jpg",".png",".jpeg",".webp"]
m_CalLibPath = "/Volumes/NewDataStor/calibre_lib/"
def Start():
pathTrg = os.path.abspath(m_CalLibPath)
if False == os.path.exists(pathTrg):

490
MgrCalibreUI.py Normal file
View File

@@ -0,0 +1,490 @@
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_())
"""

216
MgrPupilColDB.py Normal file
View File

@@ -0,0 +1,216 @@
import sqlite3
import UtilPack as util
import MgrSQLiteDB as MyDB
class MgrPupilColDB(MyDB.MgrSQLiteDB):
def __init__(self, path: str):
super().__init__(path)
self.init()
def init(self):
# 데이터베이스 연결 (파일이 없으면 새로 생성됨)
self.conn = sqlite3.connect(f'file:{self.path}?charset=UTF-8', uri=True)
self.cursor = self.conn.cursor()
# ID -> HitomiID, ArchiveID, Title, Authors, Group, Series, Type. Language, Tags, Description,
def GetBookByID(self, nID: int) -> dict:
pass
def GetArchiveBrBookID(self, nID: int ) -> list[str]:
pass
def GetAuthorByID(self, nID: int) -> list[str]:
pass
def GetTagByID(self, nID: int) -> list[str]:
pass
def GetSeriesByID(self, nID: int) -> list[str]:
pass
def GetGroupByID(self, nID: int) -> list[str]:
pass
#
def GetIDByName(self, strTableName : str, strName: str) -> int:
if True == util.IsEmptyStr(strName) or True == util.IsEmptyStr(strTableName):
return -1
strSQL = f"SELECT id FROM \'{strTableName}\' WHERE name = \'{strName}\';"
self.SQLExecute(self.cursor, strSQL, True)
result = self.cursor.fetchone()
if result is None:
return -1
return result[0]
#
def GetAuthorIDByName(self, strName: str) -> int:
return self.GetIDByName("authors", strName)
#
def GetGroupIDByName(self, strName: str) -> int:
return self.GetIDByName("groups", strName)
#
def GetLanguageIDByName(self, strName: str) -> int:
return self.GetIDByName("languages", strName)
#
def GetTagIDByName(self, strTag: str) -> int:
return self.GetIDByName("tags", strTag)
"""
["id"]
["title"]
["language"]
["type"]
["date"]
["artists"]
["artist"]
["groups"]
["group"]
["url"]
["parodys"]
["parody"]
["url"]
["tags"]
["tag"]
["url"]
["female"]
["male"]
["related"]
["languages"]
["galleryid"]
["url"]
["language_localname"]
["name"]
["characters"]
["character"]
["url"]
["files"]
"""
def AddBook(self, nHitomiID: int, strArchiveID: str, strTitle: str, listAuthors: list[str],
listGroups: list[str], listSeries: list[str], strType: str, strLanguage: str,
listTags: list[str], strDescription: str) -> int:
pass
# 작가 정보
# Language 는 작가가 작품을 주로 작성하는 언어다. 한국인이라도 일본어로 작품을 내면 일본어로 설정
def AddAuthor(self, strName: str, strGroup: str, strLanguage: str, strDesc: str) -> int:
if True == util.IsEmptyStr(strName):
return -1
# 이미 들어있나?
nAuthorID = self.GetAuthorIDByName(strName)
if nAuthorID > 0:
util.DbgOut(f"이미 존재하는 작가: {strName}", True)
return nAuthorID
# 그룹이 없으면 추가
nGroupID = self.GetGroupIDByName(strGroup)
if nGroupID <= 0:
util.DbgOut(f"그룹이 존재하지 않음: {strGroup}", True)
nGroupID = self.AddGroup(strGroup)
# 그룹 추가 확인, 여기서 잘못되면 더 위에서 잘못된 것.
if nGroupID <= 0:
util.DbgOut(f"그룹 추가 실패: {strGroup}", True)
return -1
# 언어는 오타가 나거나 모르면 unknown : 0 으로 설정
nLanguageID = self.GetLanguageIDByName(strLanguage)
if nLanguageID <= 0:
util.DbgOut(f"언어가 존재하지 않음: {strLanguage}", True)
strLanguage = "unknown"
strTableName = "authors"
try:
strSQL = f"INSERT OR IGNORE INTO \'{strTableName}\' (name, group, language, desc) VALUES (?, ?, ?, ?)"
self.cursor.execute(strSQL, (strName, strGroup, strLanguage, strDesc))
nID = self.cursor.lastrowid
self.conn.commit()
return nID if nID is not None else -1
except sqlite3.Error as e:
util.DbgOut(f"SQLite Error occurred: {e}", True)
return -1
def AddTag(self, strTag: str) -> int:
if True == util.IsEmptyStr(strTag):
return -1
nTagID = self.GetTagIDByName(strTag)
if nTagID > 0:
util.DbgOut(f"이미 존재하는 태그: {strTag}", True)
return nTagID
strSep = ""
strValue = ""
if strTag.find(":") >= 0:
strSep = strTag.split(":")[0]
strValue = strTag.split(":")[1]
return self.AddTagRaw(strTag, strSep, strValue)
def AddTagRaw(self, strName: str, strSep: str, strValue: str) -> int:
if True == util.IsEmptyStr(strName):
return -1
strTableName = "tags"
try:
strSQL = f"INSERT INTO \'{strTableName}\' (name, sep_title, value) VALUES (?)"
self.cursor.execute(strSQL, (strName, strSep, strValue,))
nID = self.cursor.lastrowid
self.conn.commit()
return nID if nID is not None else -1
except sqlite3.Error as e:
util.DbgOut(f"SQLite Error occurred: {e}", True)
return -1
def AddsSeries(self, strName: str) -> int:
pass
def AddGroup(self, strName: str) -> int:
if True == util.IsEmptyStr(strName):
return -1
strTableName = "groups"
try:
strSQL = f"INSERT INTO \'{strTableName}\' (name) VALUES (?)"
self.cursor.execute(strSQL, (strName,))
nID = self.cursor.lastrowid
self.conn.commit()
return nID if nID is not None else -1
except sqlite3.Error as e:
util.DbgOut(f"SQLite Error occurred: {e}", True)
return -1
def AddArchive(self, strName: str, setArcPath: str) -> int:
pass
# ["width"] ["hash"] ["name"] ["height"] ["hasavif"]
# filename, hash, ext, hasavif, width, height
def AddFileInfo(self, strName: str, strHash: str, bHasAvif: bool, nWidth: int, nHeight: int) -> int:
strTableName = "files"
try:
strSQL = f"INSERT INTO \'{strTableName}\' (filename, hash, hasavif, width, height) VALUES (?)"
self.cursor.execute(strSQL, (strName, strHash, bHasAvif, nWidth, nHeight))
nID = self.cursor.lastrowid
self.conn.commit()
return nID if nID is not None else -1
except sqlite3.Error as e:
util.DbgOut(f"SQLite Error occurred: {e}", True)
return -1

97
MgrSQLiteDB.py Normal file
View File

@@ -0,0 +1,97 @@
from abc import abstractmethod
import sqlite3
import UtilPack as util
class MgrSQLiteDB:
def __init__(self, path: str):
self.path = path
self.init()
def __enter__(self):
pass
def __exit__(self, ex_type, ex_value, traceback):
self.conn.close()
@abstractmethod
def init(self):
pass
# 쿼리를 실행한다. Commit 여부를 선택할 수 있다.
def SQLExecute(self, cursor: sqlite3.Cursor, strSQL: str, bCommit: bool = True):
if not cursor:
return None
if True == util.IsEmptyStr(strSQL):
return None
try:
self.cursor.execute(strSQL)
if True == bCommit:
self.conn.commit()
except sqlite3.Error as e:
util.DbgOut(f"SQLite Error occurred: {e}")
# 쿼리를 실행해서 결과를 가져온다.
# 결과는 리스트
def SQLExecute_Get(self, cursor: sqlite3.Cursor, strSQL: str):
if not cursor:
return None
if True == util.IsEmptyStr(strSQL):
return None
listRet = []
try:
self.cursor.execute(strSQL)
listRet = self.cursor.fetchall()
except sqlite3.Error as e:
util.DbgOut(f"SQLite Error occurred: {e}")
return listRet
# 테이블의 모든 데이터를 가져온다.
def GetTableAllData(self, strTableName: str):
if True == util.IsEmptyStr(strTableName):
return None
strSQL = f"SELECT * FROM \"{strTableName}\";"
return self.SQLExecute_Get(self.cursor, strSQL)
# 테이블 구조를 가져온다.
def GetTableSchm(self, cursor: sqlite3.Cursor, strTableName: str):
if True == util.IsEmptyStr(strTableName):
return None
strSQL = f"PRAGMA table_info (\"{strTableName}\");"
return self.SQLExecute_Get(self.cursor, strSQL)
# ID 에 해당하는 항목의 지정한 칼럼의 값을 변경한다.
def UpdateTableValue(self, strTableName: str, nID: int, strColName : str, strValue: str):
if 0 >= nID or True == util.IsEmptyStr(strTableName) or True == util.IsEmptyStr(strColName):
return
strSQL = f"UPDATE {strTableName} SET \'{strColName}\' = \'{strValue}\' WHERE id = {nID};"
self.SQLExecute(self.cursor, strSQL)
# 하나의 조건에 맞는 항목의 한 컬럼을 지정해서 값을 가져온다.
def GetValuesOneCondition(self, strTableName: str, strGetCol: str, strCompCol: str, strCompValue: str) -> list[str]:
if True == util.IsEmptyStr(strTableName) or True == util.IsEmptyStr(strGetCol) or True == util.IsEmptyStr(strCompCol):
return []
strSQL = f"SELECT {strGetCol} FROM {strTableName} WHERE {strCompCol} = {strCompValue};"
listRet = self.SQLExecute_Get(self.cursor, strSQL)
if listRet is None or 0 >= len(listRet):
return []
return [item[0] for item in listRet]

BIN
Res/layoutImg.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

259
UI.py
View File

@@ -1,259 +0,0 @@
import sys
import os
import UtilPack as util
from io import BytesIO
from PyQt5.QtCore import Qt, QUrl, QSettings, QSize, QPoint, QByteArray
from PyQt5.QtWidgets import QApplication, QWidget, QMainWindow, QPushButton, QVBoxLayout, QLineEdit, QHBoxLayout, QListWidget, QListWidgetItem, QLabel, QFileDialog
from PyQt5.QtGui import QPixmap, QKeyEvent
QApplication.setAttribute(Qt.AA_ShareOpenGLContexts)
class MyApp(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
self.loadINI()
def closeEvent(self, event):
self.saveINI()
event.accept()
def resizeEvent(self, event):
self.GetFitSize(self.label_Image.size())
print( self.label_Image.size() )
def initUI(self):
layout = self.MakeUI()
# 레이아웃을 윈도우에 적용
central_widget = QWidget()
central_widget.setLayout(layout)
self.setCentralWidget(central_widget)
self.setWindowTitle('Manga Database')
self.move(10, 10)
self.show()
def loadINI(self):
settings = QSettings('MyApp', 'settings')
window_size = settings.value('window/size', '800, 600')
window_position = settings.value('window/position', '100, 100')
def saveINI(self):
settings = QSettings('MyApp', 'settings')
# 키-값 형식으로 데이터 저장
settings.setValue('window/size', "1024,768")
settings.setValue('window/position', "100,100")
def MakeUI_Left(self):
self.list_SrcPath = QListWidget()
self.list_SrcPath.setMaximumHeight(400)
self.list_SrcPath.setMaximumWidth(300)
btn_Add = QPushButton("Add", self)
btn_Add.clicked.connect(self.on_click_SrcAdd)
btn_Del = QPushButton("Del", self)
btn_Del.clicked.connect(self.on_click_SrcDel)
btn_Read = QPushButton("Read!", self)
btn_Read.clicked.connect(self.on_click_SrcRead)
layout_Btns = QHBoxLayout()
layout_Btns.addWidget(btn_Add)
layout_Btns.addWidget(btn_Del)
layout_Btns.addWidget(btn_Read)
layout_Top = QVBoxLayout()
layout_Top.addWidget(self.list_SrcPath)
layout_Top.addLayout(layout_Btns)
self.list_ArcList = QListWidget(self)
self.list_ArcList.itemSelectionChanged.connect(self.on_Item_SelChanged_listArc)
self.list_Infos = QListWidget(self)
layout = QVBoxLayout()
layout.addLayout(layout_Top)
layout.addWidget(self.list_ArcList)
layout.addWidget(self.list_Infos)
return layout
def MakeUI(self):
layout_L = self.MakeUI_Left()
# show Image in middle Layout
self.label_Image = QLabel(self)
# Load Default Image
pixmap = QPixmap("layoutImg.jpeg")
self.label_Image.setMaximumWidth(self.screen().size().width()-(self.list_ArcList.width()+400))
self.list_Items = QListWidget(self)
self.list_Items.setFixedWidth(300)
self.list_Items.itemSelectionChanged.connect(self.on_Item_SelChanged_Items)
# 레이아웃 설정
layout = QHBoxLayout()
layout.addLayout(layout_L)
layout.addWidget(self.label_Image)
layout.addWidget(self.list_Items)
return layout
def GetFitSize(self, szTarget):
szDesktop = self.screen().size()
szWindow = self.size()
nTrgHeight = self.list_Items.height()
nTrgWidth = szWindow.width() - ( self.list_ArcList.width() + self.list_Items.width() )
def on_click_SrcAdd(self):
# 폴더 선택 창을 띄움
folder_path = QFileDialog.getExistingDirectory(self, '폴더 선택', '')
if None == folder_path or True == util.IsEmptyStr(folder_path):
return
# 소스 폴더 목록에 추가
self.list_SrcPath.addItem(folder_path)
# 폴더 내의 자식 폴더 목록을 가져온다.
listFolders = util.ListSubDirectories(folder_path)
# 선택한 폴더도 리스트에 추가한다.
listFolders.append(folder_path)
for folder in listFolders:
# 폴더 내의 파일 목록을 가져온다.
listFiles = util.ListContainFiles(folder)
# 파일 목록을 훑어서 내용을 판단
# 1. 압축파일이 들어있나?
# 2. 이미지 파일이 들어있나?
isImgIn = False
for pathFile in listFiles:
filename = util.GetParentDirName(pathFile, 0)
fileExt = util.GetExtStr(filename)
# 압축파일이 들어있다면...
if fileExt.lower() in [".zip", ".cbz", ".rar"]:
# 이름은 폴더/압축파일
FolderName = util.GetParentDirName(pathFile, 1)
ItemName = os.path.join(FolderName, filename)
item = QListWidgetItem(ItemName)
# 압축파일의 전체 경로를 따로 저장
item.setData(Qt.UserRole, pathFile)
self.list_ArcList.addItem(item)
# 데이터베이스 파일이 들어 있다면...
if fileExt.lower() in [".db"]:
# 이름은 폴더/DB 파일이름
FolderName = util.GetParentDirName(pathFile, 1)
ItemName = os.path.join(FolderName, filename)
item = QListWidgetItem(ItemName)
# 전체 경로를 따로 저장
item.setData(Qt.UserRole, pathFile)
self.list_ArcList.addItem(item)
# 이미지 파일이 들어있다면...
if fileExt.lower() in [".jpg", ".webp", ".jpeg", ".png", ".gif"]:
isImgIn = True
if True == isImgIn:
# 이름은 폴더
FolderName = util.GetParentDirName(folder, 0)
item = QListWidgetItem(FolderName)
# 폴더 경로를 따로 저장
item.setData(Qt.UserRole, folder)
self.list_ArcList.addItem(item)
def on_click_SrcDel(self):
items = self.list_SrcPath.selectedItems()
if not items:
return
selItemText = ""
for item in items:
selItemText = item.text()
row = self.list_SrcPath.row(item)
self.list_SrcPath.takeItem(row)
def on_click_SrcRead(self):
pass
def on_Item_SelChanged_listArc(self):
items = self.list_ArcList.selectedItems()
self.list_Infos.clear()
pathTarget = None
for item in items:
pathTarget = item.data(Qt.UserRole)
if False == os.path.exists(pathTarget):
return
# 압축파일일 경우...
listContents = []
if True == os.path.isfile(pathTarget):
fileExt = util.GetExtStr(pathTarget)
# 일단 zip 만...
if fileExt.lower() in [".zip", ".cbz"]:
listContents = util.GetZipContentList(pathTarget)
elif fileExt.lower() == ".rar":
listContents = []
elif True == os.path.isdir(pathTarget):
listContents = util.ListFileExtRcr(pathTarget, [".jpg", ".jpeg", ".png", ".webp"])
self.list_Infos.addItem( QListWidgetItem(pathTarget) )
self.list_Infos.addItem( QListWidgetItem( f"{len(listContents)}" ) )
self.list_Items.clear()
self.list_Items.addItems( sorted( listContents ) )
def on_Item_SelChanged_Items(self):
items = self.list_Items.selectedItems()
if 0 >= len(items):
return
selItemText = items[0].text()
pixmap = QPixmap()
pathTarget = self.list_Infos.item(0).text()
if True == os.path.isfile(pathTarget):
data = util.GetZippedFileByte(pathTarget, selItemText)
imageIO = BytesIO(data)
pixmap.loadFromData( QByteArray( imageIO.getvalue() ) )
elif True == os.path.isdir(pathTarget):
if True == os.path.exists(selItemText):
pixmap.load(selItemText)
self.label_Image.setPixmap(pixmap)
if __name__ == '__main__':
app = QApplication(sys.argv)
app.setQuitOnLastWindowClosed(True)
main_window = MyApp()
#main_window.show()
sys.exit(app.exec_())

View File

@@ -1,4 +1,5 @@
import os
import re
import time
import uuid
import rarfile
@@ -6,19 +7,22 @@ import zipfile
import shutil
import difflib
import subprocess
import hashlib
from pathlib import Path
m_dbgLevel = 0
listDbgStr = []
listDbgStr: list[str] = []
#
def IsEmptyStr(string):
return 0 == len(string.strip())
def IsEmptyStr(string: str) -> bool:
temp = f"{string}"
return 0 == len(temp.strip())
#
def GetCurrentTime():
def GetCurrentTime() -> str:
# 현재 시간을 구하고 구조체로 변환
current_time_struct = time.localtime()
@@ -30,27 +34,40 @@ def GetCurrentTime():
minute = current_time_struct.tm_min
second = current_time_struct.tm_sec
strRet = (f"{year}/{month}/{day}_{hour}:{minute}:{second}")
strRet = (f"{year}-{month}-{day}_{hour}:{minute}:{second}")
return strRet
#for debug
def DbgOut(strInput, bPrint = False):
def DbgOut(strInput:str, bPrint:bool = False):
strMsg = (f"{GetCurrentTime()} : {strInput}")
listDbgStr.append(strMsg)
if True == bPrint:
print(strMsg)
#
def printDbgMessages():
for line in listDbgStr:
print(line)
#
def SaveDbgMessages(Path: str):
try:
with open(Path, 'w') as file:
for line in listDbgStr:
file.write(line + "\n")
except IOError:
DbgOut(f"Error: Could not write to the file at {Path}.", True)
# 입력된 경로의 자식 폴더를 찾아 반환한다.
# 반환하는 리스트는 리커시브 - 손자, 증손자 폴더까지 전부 포함한다
def ListSubDirectories(root_dir):
subdirectories = []
def ListSubDirectories(root_dir:str)-> list[str]:
subdirectories: list[str] = []
# root_dir에서 하위 디렉토리 및 파일 목록을 얻음
for dirpath, dirnames, filenames in os.walk(root_dir):
@@ -64,31 +81,36 @@ def ListSubDirectories(root_dir):
return subdirectories
# 자식 폴더를 구해온다. 직계 자식만
def ListChildDirectories(pathDir):
listRet = []
for name in os.listdir(pathDir):
def ListChildDirectories(pathDir:str, bVisibleOnly: bool = True) -> list[str]:
listRet:list[str] = []
listTemp = os.listdir(pathDir)
for name in listTemp:
pathChild = os.path.join(pathDir, name)
if os.path.isdir(pathChild):
listRet.append(pathChild)
if True == bVisibleOnly and name.startswith('.'):
continue
listRet.append(name)
return listRet
# 파일목록만 구해온다. 자식 폴더에 있는건 무시.
def ListContainFiles(pathDir):
listRet = []
for name in os.listdir(pathDir):
# PathDir 에 지정된 폴더의 파일목록만 구해온다. 자식 폴더에 있는건 무시.
def ListContainFiles(pathDir:str, bVisibleOnly:bool = True)-> list[str]:
listRet:list[str] = []
listTemp = os.listdir(pathDir)
for name in listTemp:
pathChild = os.path.join(pathDir, name)
if not os.path.isdir(pathChild):
listRet.append(pathChild)
if os.path.isfile(pathChild):
if True == bVisibleOnly and name.startswith('.'):
continue
listRet.append(name)
return listRet
def ListFileExtRcr(pathTrg, listExt):
listRet= []
if False == isinstance(listExt, list):
print("ext must list")
return
# 리스트에 담긴 확장자와 같은 확장자를 가진 파일을 찾아서 리스트로 반환
def ListFileExtRcr(pathTrg: str, listExt: list[str]) -> list[str]:
listRet:list[str] = []
# pathTrg의 하위 디렉토리 및 파일 목록을 얻음
for dirpath, dirnames, filenames in os.walk(pathTrg):
@@ -99,9 +121,8 @@ def ListFileExtRcr(pathTrg, listExt):
return listRet
# 입력된 패스에서 부모 폴더를 찾는다. 0 은 자기자신 1은 부모, 숫자대로 위로.
def GetParentDirName(FullPath, nUp):
# 입력된 경로에서 부모 폴더를 찾는다. 0 은 자기자신 1은 부모, 숫자대로 위로.
def GetParentDirName(FullPath : str, nUp : int)->str:
parts = FullPath.split(os.sep)
nTrgIdx = 0
@@ -114,27 +135,26 @@ def GetParentDirName(FullPath, nUp):
return parts[nTrgIdx]
# 입력된 경로가 자식 폴더를 가지고 있는지 판단한다.- 최종 폴더인지 여부
# 자식이 없으면 True, 자식이 있으면 False
def IsFinalFolder(path):
def IsFinalFolder(path : str) -> bool:
bRet = True
contents = os.listdir(path)
for item in contents:
if True == os.path.isdir(item):
bRt = False
bRet = False
break
return bRet;
# 어떤 경로 안에서 특정 확장자의 파일을 뽑아내어 그 리스트를 반환한다.
def FindFileFromExt(path, ext):
def FindFileFromExt(path: str, ext: str)-> list[str]:
bDot = False
if 0 <= ext.find('.'):
bDot = True
listRet = []
listRet:list[str] = []
if False == os.path.exists(path):
return listRet
@@ -150,7 +170,7 @@ def FindFileFromExt(path, ext):
return listRet
# 파일 이름에서 확장자를 뽑아낸다. True : '.' 을 포함한다.
def GetExtStr(file_path, bDot = True):
def GetExtStr(file_path: str, bDot: bool = True)-> str:
retStr = ""
# 파일 경로에서 마지막 점을 찾아 확장자를 추출
last_dot_index = file_path.rfind('.')
@@ -165,7 +185,7 @@ def GetExtStr(file_path, bDot = True):
return retStr
# 문자열에 포함된 단어를 지운다.
def RmvSubString(mainString, subString):
def RmvSubString(mainString: str, subString: str)-> str:
# 문자열에서 부분 문자열의 인덱스를 찾습니다.
strIdx = mainString.find(subString)
if strIdx == -1: # 부분 문자열이 존재하지 않으면 그대로 반환합니다.
@@ -176,12 +196,13 @@ def RmvSubString(mainString, subString):
# 부분 문자열을 제거하고 새로운 문자열을 반환합니다.
return mainString[:strIdx] + mainString[endIdx:]
def ExtractZIP(zip_file, extract_to):
#
def ExtractZIP(zip_file: str, extract_to: str):
with zipfile.ZipFile(zip_file, 'r') as zf:
zf.extractall(extract_to)
#
def CreateZIP(output_zip, *files):
def CreateZIP(output_zip: str, files: list[str]) -> bool:
with zipfile.ZipFile(output_zip, 'w') as zf:
for file in files:
pathTemp = os.path.join('root', os.path.basename(file))
@@ -194,7 +215,7 @@ def CreateZIP(output_zip, *files):
return bRet
# 파일 리스트에 들어있는 파일만 골라서 압축을 합니다. 상대경로를 제거하는게 기본값.
def CreateZIPShell(zipName, *files, bRmvRPath = True):
def CreateZIPShell(zipName: str, files: list[str], bRmvRPath: bool = True) -> bool:
command = "zip "
if True == bRmvRPath:
@@ -204,17 +225,7 @@ def CreateZIPShell(zipName, *files, bRmvRPath = True):
# 이중 리스트인 이유를 모르겠다.
for file in files:
strTemp = ""
if isinstance(file, list):
strTemp = ' '.join(file)
else:
strTemp = f"\"{file}\" "
command += strTemp
# for item in file:
# command += f"\"{item}\" "
command += f"\"{file}\" "
result = subprocess.run(command, shell=True, capture_output=True, text=True)
@@ -225,7 +236,7 @@ def CreateZIPShell(zipName, *files, bRmvRPath = True):
return bRet
# 특정 확장자만 쉘을 이용해서 압축한다
def CreateZIPShExt(zipName, TrgExt):
def CreateZIPShExt(zipName: str, TrgExt: str)-> bool:
command = f"zip -j {zipName} *.{TrgExt}"
result = subprocess.run(command, shell=True, capture_output=True, text=True)
@@ -237,8 +248,8 @@ def CreateZIPShExt(zipName, TrgExt):
return bRet
# 압축 파일 내의 모든 파일 및 디렉토리 목록 가져오기
def GetZipContentList(path):
if None == path or not os.path.isfile(path):
def GetZipContentList(path: str) -> list[str]:
if True == IsEmptyStr(path) or not os.path.isfile(path):
return []
listRet = []
@@ -247,23 +258,19 @@ def GetZipContentList(path):
return listRet
def GetZippedFileByte(pathZip, FileName):
if None == pathZip or not os.path.isfile(pathZip):
return None
retBytes = None
#
def GetZippedFileByte(pathZip: str, FileName: str) -> bytes:
retBytes:bytes = bytes()
if True == os.path.isfile(pathZip):
with zipfile.ZipFile( pathZip , 'r') as zip_file:
if not FileName in zip_file.namelist():
return None
# 압축 파일 내의 특정 파일을 읽기
with zip_file.open(FileName) as file:
retBytes = file.read()
return retBytes
# JSON 을 트리 구조로 출력한다.
def PrintJSONTree(data, indent=0):
def PrintJSONTree(data, indent: int=0 ) -> None:
if isinstance(data, dict):
for key, value in data.items():
print(' ' * indent + str(key))
@@ -274,6 +281,7 @@ def PrintJSONTree(data, indent=0):
else:
print(' ' * indent + str(data))
#
def IsPathWithin(base_path: str, target_path: str) -> bool:
base = Path(base_path).resolve()
target = Path(target_path).resolve()
@@ -285,7 +293,54 @@ def UUIDGenRandom():
return random_uuid
# 이름을 기반으로 UUID 생성
def UUIDGenName(SeedName):
def UUIDGenName(SeedName:str):
namespace_uuid = uuid.uuid5(uuid.NAMESPACE_DNS, SeedName)
return namespace_uuid
#
def GetTextInBrakets(text:str)-> list[str]:
return re.findall(r'\[(.*?)\]', text)
#파일의 해시를 계산하는 함수.
#Args:
# strFilePath (str): 파일 경로
# method (str): 사용할 해시 알고리즘 ('md5', 'sha1', 'sha256' 등)
#Returns:
# str: 계산된 해시값 (16진수 문자열)
def CalculateFileHash(strFilePath: str, method: str="sha256")-> str:
funcHash = getattr(hashlib, method)()
with open(strFilePath, "rb") as f:
while True:
chunk = f.read(4096)
if not chunk:
break
funcHash.update(chunk)
return funcHash.hexdigest()
#파일의 해시를 비교하여 무결성 검증.
#Args:
# file_path (str): 파일 경로
# expected_hash (str): 기대하는 해시값
# method (str): 사용할 해시 알고리즘
#Returns:
# bool: 파일이 정상인지 여부
def VerifyIsValidFile(strPath: str, strCompHash: str, strMethod: str="sha256")->bool:
Hash_Calcd = CalculateFileHash(strPath, strMethod)
return strCompHash.lower() == Hash_Calcd.lower()
"""
# 사용 예시
if __name__ == "__main__":
file_to_check = "example.txt"
known_good_hash = "5d41402abc4b2a76b9719d911017c592" # 예시 (MD5)
is_valid = verify_file(file_to_check, known_good_hash, method='md5')
if is_valid:
print("파일이 정상입니다!")
else:
print("파일이 손상되었거나 다릅니다!")
"""

76
main.py
View File

@@ -1,74 +1,20 @@
import GetArc_Hitomi as getHitomi
import GetArc_Ehentai as getEhentai
import MgrCalibreLibs as mgrCal
import UtilPack as util
import StoreXLS as xls
import os
import sys
import MgrCalibreUI
from PyQt5.QtWidgets import QApplication
def main(argc, argv):
if argc != 2:
printUsage()
return
app = QApplication(argv)
trgPath = argv[1]
#getHitomi.GetSearchResult("2890685")
#etEhentai.GetSearchResult("artist%3A%22kotomi+yo-ji%24%22")
app.setQuitOnLastWindowClosed(True)
main_window = MgrCalibreUI.MyApp()
main_window.show()
#mgrCal.Start()
#util.printDbgMessages()
sys.exit(app.exec_())
#artist:"kotomi yo-ji$"
#"artist%3A%22kotomi+yo-ji%24%22"
#tempxls = xls.DBXLStorage("./temp.xls")
#tempxls.DBXLSOpen()
#tempxls.AddArtistInfo("Kuno Inu", "/artist/kunoinu/")
#tempxls.AddSeriesInfo("Original", "/serires/original/")
#tempxls.AddTagInfo("female:bondage", "/tag/bondage/")
#tempxls.AddTagInfo("female:slave", "/tag/slave/")
#tempxls.DBXLSClose()
if False == os.path.exists(trgPath):
printUsage()
return
listPaths = util.ListSubDirectories(trgPath)
util.DbgOut(f"Folder Cnt : {len(listPaths)}")
PupilDB_EXT = ".data"
PupilImg_EXT = ".img"
CalbCvr = "cover"
CalbCvr_EXT = ".jpg"
nIdx = 0
# 각 pupil 만화책 폴더 -> path
for path in listPaths:
# 퍼필에서 저장한 만화책 폴더의 절대경로를 구한다
# 퍼필 만화책 정보 파일을 파싱
# - 만약 만화책 파일이 하나도 없거나 정보 파일과 다르다면..
# - - 정보 파일만 적당히 백업하고 다음으로 넘어간다
#
# 캘리버 정보 파일로 저장한다
# 퍼필에서 생성한 표지 파일을 찾는다.
# webp 인지 확인해서 jpg 로 변환하여 저장, JPG 라면 이름만 바꾼다
# 만약 없다면 1번 파일을 적당히 줄여서 표지 파일을 생성
# 이미지 파일 리스트를 생성
# 압축한다
# 압축파일, 캘리버 정보 파일, 표지 파일이 다 있는지 확인하고 메시지 출력
# 하나라도 없으면 에러 리스트에 저장
nIdx += 1
def printUsage():
print("Usage : python main.py <DataFilesFolderPath>")
# For Main Loop
if __name__ == '__main__':
@@ -76,3 +22,5 @@ if __name__ == '__main__':
argv = sys.argv
main(argc, argv)

View File

@@ -1,93 +0,0 @@
import json
import UtilPack as util
import DataClass as info
GALBLOCK = "galleryBlock"
GALURL = "galleryUrl"
GALINFO = "galleryInfo"
GALTAGS = "relatedTags"
JTITLE = "japanese_title"
# Example
#with open('test.db', 'r') as file:
# data = json.load(file)
#print_json_tree(data)
#print(data['galleryInfo']['tags'])
# pupil : Json
# Caribre : text
# My : CSV
class PupuilInfoFile:
m_data = None
def __init__(self, path):
self.path = path
def __enter__(self):
self.DBXLSOpen(self.path)
def __exit__(self, ex_type, ex_value, traceback):
self.DBXLSClose()
def PupilJSONOpen(self, path):
with open(path, 'r') as file:
self.m_data = json.load(file)
# pupil 의 JSON 을 파싱해서 DataClass 에 데이터를 넣어 반환한다.
def GetInfo(self):
if None == self.m_data:
return None
title = self.m_data[GALINFO]["title"]
url = self.m_data[GALBLOCK]["galleryUrl"]
retInfo = info(title, url)
retInfo.type = self.m_data[GALINFO]["type"]
retInfo.language = self.m_data[GALINFO]["language"]
retInfo.gallery_id = self.m_data[GALINFO]["id"]
listArtists = self.m_data[GALINFO]["artists"]
for item in listArtists:
strArtist = item["artist"]
strUrl = item["url"]
strTag = f"artist:{strArtist}"
tempInfo = util.TagInfo(strTag, strUrl)
retInfo.AddArtist(tempInfo)
listTags = self.m_data[GALINFO]["tags"]
for item in listTags:
strGend = ""
if 1 == item["female"]:
strGend = "female:"
elif 1 == item["male"]:
strGend = "male:"
strTag = item["tag"]
strRelatedTag = f"{strGend}:{strTag}"
tagUrl = item[url]
tempInfo = util.TagInfo(strRelatedTag, tagUrl)
retInfo.AddTag(tempInfo)
return retInfo
# pupil 의 JSON 을 파싱해서 ImageFileList 를 반환한다.
def GetImageFilesInfo(self):
if None == self.m_data:
return None
listRet = set()
listFiles = self.m_data[GALINFO]["files"]
for item in listFiles:
tempInfo = info.ImageFileInfo(item["name"],
item["height"],
item["width"],
item["hash"],
item["haswebp"])
listRet.append(tempInfo)
return listRet

View File

@@ -1,181 +0,0 @@
import os
import rarfile
import zipfile
import shutil
import difflib
img_exts = [".jpg",".png",".jpeg",".webp"]
TrgBasePath = "/Volumes/NewDataStor/Backup/files/"
m_Debug = False
def main(debug=False):
m_Debug = debug
TrgBasePath = os.path.abspath("/Volumes/NewDataStor/Backup/files/")
# 설정한 경로가 유효한가?
if False == os.path.exists(TrgBasePath):
print("Not Valid Path")
return
# 압축을 죄다 푼다.
# -> 일단 이 과정은 생략
# 폴더만 죄다 골라내서..
listTrgPaths = ListSubDirectories(TrgBasePath)
dbgmsg(listTrgPaths)
for path in listTrgPaths:
#이미지 파일만 골라 리스트업.
contents = os.listdir(path)
listImgFiles = []
for item in contents:
if True == os.path.isdir(item):
continue
ext = get_extension_str(item)
if ext in img_exts:
if os.path.exists(item):
listImgFiles.append(item)
else:
strTemp = os.path.join(path, item)
listImgFiles.append(strTemp)
dbgmsg(listImgFiles)
if 0 >= len(listImgFiles):
continue
# 폴더 이름을 조합해서 파일 이름을 만든다.
strArcName = MakeCBZName(path) + ".cbz"
strArcPath = os.path.join(TrgBasePath, strArcName)
# 이미 있으면 패스
if os.path.exists(strArcName):
continue
else:
CreateZIP(strArcPath, *listImgFiles)
# 폴더를 압축한다.
# 폴더 안의 파일을 정리한다. .url, .txt, .db 뺸다
# 일단 test.zip 로 압축하고, 원래 폴더 이름으로 변경
# 만약 작가폴더 안에 작품이 들어있다면...
# 이걸 알 수는 없으니 그냥 앞에 대괄호 넣고 폴더 이름을 붙인다.
# 가능하면 하위 폴더 이름에 작가 이름이 들어있다면 그냥 생략하자. 가능하다면...
def MakeCBZName(path):
strSrcPath = os.path.abspath(path)
if len(strSrcPath) < len(TrgBasePath) or TrgBasePath == strSrcPath:
return strSrcPath.replace("/", "-")
strTemp = RmvSubString(strSrcPath, TrgBasePath)
listPar = strTemp.split(os.sep)
strRet = listPar.pop();
for item in reversed(listPar):
IdxTmp = strRet.find(item)
if IdxTmp == -1:
strRet = "[" + item + "]" + strRet
else:
continue
return strRet
#
def RmvSubString(main_string, substring):
# 문자열에서 부분 문자열의 인덱스를 찾습니다.
start_index = main_string.find(substring)
if start_index == -1: # 부분 문자열이 존재하지 않으면 그대로 반환합니다.
return main_string
end_index = start_index + len(substring)
# 부분 문자열을 제거하고 새로운 문자열을 반환합니다.
return main_string[:start_index] + main_string[end_index:]
#
def dbgmsg(string):
if True == m_Debug:
print(string)
#
def ExtractRAR(file_path, extract_path):
with rarfile.RarFile(file_path, 'r') as rf:
rf.extractall(extract_path)
#
def ExtractZIP(zip_file, extract_to):
with zipfile.ZipFile(zip_file, 'r') as zf:
zf.extractall(extract_to)
#
def CreateZIP(output_zip, *files):
with zipfile.ZipFile(output_zip, 'w') as zf:
for file in files:
zf.write(file, os.path.basename(file))
bRet = False
if os.path.exists(output_zip):
bRet = True
return bRet
#
def ListSubDirectories(root_dir):
subdirectories = []
# root_dir에서 하위 디렉토리 및 파일 목록을 얻음
for dirpath, dirnames, filenames in os.walk(root_dir):
# 하위 디렉토리 목록을 반복하며 하위 디렉토리만 추출
for dirname in dirnames:
path = os.path.join(dirpath, dirname)
if True == IsFinalFolder(path):
subdirectories.append(path)
return subdirectories
#
def IsFinalFolder(path):
bRet = True
contents = os.listdir(path)
for item in contents:
if True == os.path.isdir(item):
bRt = False
break
return bRet;
#
def get_extension_str(file_path):
# 파일 경로에서 마지막 점을 찾아 확장자를 추출
last_dot_index = file_path.rfind('.')
if last_dot_index == -1:
return "" # 점이 없는 경우 확장자가 없음
else:
return file_path[last_dot_index:]
#
def isRAR(file):
if True == os.path.isdir(file):
return False
if ".rar" != get_extension_str(file):
return False
return True
#
def ReExt(path, srcExt, trgExt):
nCnt = -1
return nCnt
# For Main Loop
if __name__ == '__main__':
main(False)

View File

@@ -1,9 +1,14 @@
beautifulsoup4==4.12.3
bs4==0.0.2
PyQt5==5.15.11
PyQt5-Qt5==5.15.2
PyQt5_sip==12.15.0
rarfile==4.2
setuptools==75.6.0
soupsieve==2.6
wheel==0.45.1
beautifulsoup4>=4.12.3
image>=1.5.33
jedi>=0.19.1
numpy>=1.26.4
openpyxl>=3.1.2
pandas>=2.2.2
PyQt5>=5.15.11
PyQt6>=6.7.0
python-dateutil>=2.9.0.post0
pytz>=2024.1
pyzmq>=25.1.2
rarfile>=4.2
selenium>=4.19.0
sqlparse>=0.5.0