Compare commits
10 Commits
1e06484b4e
...
origin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
061ee37ba2 | ||
| 9d98b8d19b | |||
| 2c36327ae8 | |||
| d5f2d82bc9 | |||
| f1345e2770 | |||
| 5d5c0ff675 | |||
| 07ad09bb50 | |||
| 6fe1cf8da0 | |||
| 7587da53b3 | |||
| 1c840a1e25 |
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1,5 +1,4 @@
|
|||||||
venv_mangainfo/
|
venv_mangainfo/
|
||||||
mangaDB.xlsx
|
Data/
|
||||||
temp.db
|
Temp/
|
||||||
temp.json
|
__pycache__/
|
||||||
untitled0.py
|
|
||||||
|
|||||||
123
DataClass.py
123
DataClass.py
@@ -1,49 +1,102 @@
|
|||||||
class CBZInfo:
|
from typing import Set
|
||||||
def __init__(self, title, url):
|
import UtilPack as util
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
class TagInfo:
|
class TagInfo:
|
||||||
def __init__(self, name, url):
|
def __init__(self, name: str, url: str):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.url = url
|
self.url = url
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.name} : {self.url}"
|
return f"#{self.name} : {self.url}"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ImageFileInfo:
|
class ImageFileInfo:
|
||||||
def __init__(self, name, height, width, hashValue, bWebp):
|
def __init__(self, path: str, height: int, width: int, hashValue: str, bWebp: bool):
|
||||||
self.name = name
|
self.path = path
|
||||||
self.height = height
|
self.height = height
|
||||||
self.width = width
|
self.width = width
|
||||||
self.hashValue = hashValue
|
self.hashValue = hashValue
|
||||||
self.bWebp = bWebp
|
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
190
DataClass_Pupil.py
Normal 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)
|
||||||
|
|
||||||
297
GetArc_Hitomi.py
297
GetArc_Hitomi.py
@@ -5,139 +5,210 @@ from selenium.webdriver.support import expected_conditions as EC
|
|||||||
from selenium.common.exceptions import TimeoutException
|
from selenium.common.exceptions import TimeoutException
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
import time
|
||||||
import UtilPack as util
|
import UtilPack as util
|
||||||
import DataClass as info
|
import DataClass as info
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
def GetSearchResult(searchWord):
|
class GetArc_Hitomi:
|
||||||
url = getSiteUrl(searchWord)
|
m_strBaseURL = "https://hitomi.la/"
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
util.DbgOut("Hitomi : " + url)
|
strURL = ""
|
||||||
|
if strWord.isdigit():
|
||||||
driver = webdriver.Chrome()
|
strURL = self.getSiteUrlForGallery(int(strWord))
|
||||||
driver.get(url)
|
else:
|
||||||
|
strURL = self.getSiteUrlForSearch(strWord)
|
||||||
# 웹페이지가 로드될 때까지 기다리기
|
|
||||||
try:
|
util.DbgOut(f"Hitomi : {strURL}", True)
|
||||||
WebDriverWait(driver, 30).until(
|
|
||||||
EC.presence_of_element_located((By.CLASS_NAME, 'lillie'))
|
driver = webdriver.Chrome()
|
||||||
)
|
driver.get(strURL)
|
||||||
except TimeoutException:
|
|
||||||
util.DbgOut("페이지가 로드되지 않았거나 요소를 찾을 수 없습니다.")
|
# 웹페이지가 로드될 때까지 기다리기
|
||||||
|
try:
|
||||||
|
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("페이지가 로드되지 않았거나 요소를 찾을 수 없습니다.", True)
|
||||||
|
driver.quit()
|
||||||
|
return
|
||||||
|
|
||||||
|
strContent = driver.page_source
|
||||||
driver.quit()
|
driver.quit()
|
||||||
return
|
|
||||||
|
if True == bSaveHTML:
|
||||||
|
strFileName = f"{strWord}_result.html"
|
||||||
|
with open(strFileName, "w", encoding="utf-8") as file:
|
||||||
|
file.write(strContent)
|
||||||
|
|
||||||
|
util.DbgOut(f"HTML content saved to {strFileName}", True)
|
||||||
|
|
||||||
strContent = driver.page_source
|
listRet = self.parseMangaInfos(strContent)
|
||||||
driver.quit()
|
|
||||||
|
|
||||||
parseMangaInfos(strContent)
|
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
||||||
#
|
|
||||||
def getSiteUrl(searchWord):
|
|
||||||
strRet = "https://hitomi.la/"
|
|
||||||
|
|
||||||
if False == util.IsEmptyStr(searchWord):
|
|
||||||
if False == searchWord.isdigit():
|
|
||||||
strRet = strRet + "search.html?" + searchWord
|
|
||||||
else:
|
|
||||||
strRet = strRet + "galleries/" + searchWord + ".html"
|
|
||||||
|
|
||||||
return strRet
|
|
||||||
|
|
||||||
#
|
|
||||||
def parseMangaInfos(html_doc):
|
|
||||||
# BeautifulSoup 객체 생성
|
|
||||||
soup = BeautifulSoup(html_doc, 'html.parser')
|
|
||||||
gallery_elements = soup.find_all(class_='gallery-content')
|
|
||||||
|
|
||||||
listDJs = []
|
|
||||||
for element in gallery_elements:
|
|
||||||
listDJ = djParse(element)
|
|
||||||
listDJs.extend(listDJ)
|
|
||||||
|
|
||||||
print(len(listDJs))
|
for Idx in range(len(listRet)):
|
||||||
|
util.DbgOut(f"{Idx} : {listRet[Idx]}", True)
|
||||||
|
|
||||||
|
#
|
||||||
|
def GetListSearchResult(self, listID: list[int], bSave: bool = False):
|
||||||
|
driver = webdriver.Chrome()
|
||||||
|
|
||||||
|
# 웹페이지가 로드될 때까지 기다리기
|
||||||
|
try:
|
||||||
|
for nID in listID:
|
||||||
|
strURL = self.getSiteUrlForGallery(nID)
|
||||||
|
util.DbgOut(f"Hitomi : {strURL}", True)
|
||||||
|
|
||||||
|
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 djParse(soup_element):
|
def getSiteUrlForSearch(self, searchWord: str) -> str:
|
||||||
childs = soup_element.find_all(class_='dj')
|
return f"{self.m_strBaseURL}search.html?{searchWord}"
|
||||||
|
|
||||||
listInfos = []
|
def getSiteUrlForGallery(self, nHitomiID: int) -> str:
|
||||||
for child in childs:
|
return f"{self.m_strBaseURL}galleries/{nHitomiID}.html"
|
||||||
info = djTitleParse(child)
|
|
||||||
|
|
||||||
listTag1 = djArtistParse(child, info)
|
#
|
||||||
listTag2 = djDescParse(child, info)
|
def parseMangaInfos(self, html_doc : str) -> list[info.CBZInfo]:
|
||||||
|
# BeautifulSoup 객체 생성
|
||||||
|
soup = BeautifulSoup(html_doc, 'html.parser')
|
||||||
|
gallery_elements = soup.find_all(class_='gallery-content')
|
||||||
|
|
||||||
listInfos.append(info)
|
listDJs: list[info.CBZInfo] = []
|
||||||
|
for element in gallery_elements:
|
||||||
|
listDJ = self.djParse(element)
|
||||||
|
listDJs.extend(listDJ)
|
||||||
|
|
||||||
return listInfos
|
return listDJs
|
||||||
|
|
||||||
|
#
|
||||||
def djTitleParse(input_element):
|
def djParse(self, soup_element) -> list[info.CBZInfo]:
|
||||||
element = input_element.find('h1', class_='lillie')
|
childs = soup_element.find_all(class_='dj')
|
||||||
title = element.text
|
|
||||||
|
|
||||||
a_tag = element.find('a')
|
|
||||||
url = a_tag.get('href')
|
|
||||||
|
|
||||||
util.DbgOut("title : " + title)
|
|
||||||
util.DbgOut("URl : " + url)
|
|
||||||
|
|
||||||
return info.CBZInfo(title, url)
|
|
||||||
|
|
||||||
|
|
||||||
def djArtistParse(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
|
listInfos: list[info.CBZInfo] = []
|
||||||
|
for child in childs:
|
||||||
|
info = self.djTitleParse(child)
|
||||||
|
self.djArtistParse(child, info)
|
||||||
|
self.djDescParse(child, info)
|
||||||
|
|
||||||
|
listInfos.append(info)
|
||||||
|
|
||||||
|
return listInfos
|
||||||
|
|
||||||
|
#
|
||||||
|
def djTitleParse(self, input_element):
|
||||||
|
element = input_element.find('h1', class_='lillie')
|
||||||
|
strTitle: str = element.text
|
||||||
|
|
||||||
def djDescParse(input_element, retPtr):
|
a_tag = element.find('a')
|
||||||
element = input_element.find('table', class_='dj-desc')
|
strURL: str = a_tag.get('href')
|
||||||
tb_rows = element.find_all('tr')
|
|
||||||
listTags = []
|
|
||||||
for row in tb_rows:
|
|
||||||
tds = row.find_all('td')
|
|
||||||
if 2 != len(tds):
|
|
||||||
util.DbgOut("Warning : td get failed")
|
|
||||||
continue
|
|
||||||
|
|
||||||
|
#util.DbgOut("title : " + title)
|
||||||
|
#util.DbgOut("URl : " + url)
|
||||||
|
|
||||||
outMsg = f"{tds[0].text} : \r\n"
|
return info.CBZInfo(strTitle, strURL)
|
||||||
|
|
||||||
|
#
|
||||||
|
def djArtistParse(self, input_element, retPtr):
|
||||||
|
element = input_element.find('div', class_='artist-list')
|
||||||
|
|
||||||
a_tags = tds[1].find_all('a')
|
a_tags = element.find_all('a')
|
||||||
for tag in a_tags:
|
for tag in a_tags:
|
||||||
tag_name = tag.text
|
artist = tag.text
|
||||||
tag_url = tag.get('href')
|
a_url = tag.get('href')
|
||||||
|
retPtr.AddArtist(artist)
|
||||||
retPtr.AddTag(tag_name)
|
|
||||||
|
|
||||||
listTags.append(info.TagInfo(tag_name, tag_url))
|
|
||||||
|
|
||||||
outMsg += f" {tag_name} {tag_url}\r\n"
|
|
||||||
|
|
||||||
util.DbgOut(outMsg)
|
|
||||||
|
|
||||||
#
|
|
||||||
if "Series" == tds[0]:
|
|
||||||
retPtr.serires = listTags[-1].name
|
|
||||||
elif "Type" == tds[0]:
|
|
||||||
retPtr.type = listTags[-1].name
|
|
||||||
elif "Language" == tds[0]:
|
|
||||||
retPtr.language = listTags[-1].name
|
|
||||||
else:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return listTags
|
|
||||||
|
|
||||||
|
#
|
||||||
|
def djDescParse(self, input_element, retPtr):
|
||||||
|
element = input_element.find('table', class_='dj-desc')
|
||||||
|
tb_rows = element.find_all('tr')
|
||||||
|
listTags = []
|
||||||
|
for row in tb_rows:
|
||||||
|
tds = row.find_all('td')
|
||||||
|
if 2 != len(tds):
|
||||||
|
util.DbgOut("Warning : td get failed")
|
||||||
|
continue
|
||||||
|
|
||||||
|
outMsg = f"{tds[0].text} : \r\n"
|
||||||
|
|
||||||
|
a_tags = tds[1].find_all('a')
|
||||||
|
for tag in a_tags:
|
||||||
|
tag_name = tag.text
|
||||||
|
tag_url = tag.get('href')
|
||||||
|
|
||||||
|
retPtr.AddTag(tag_name)
|
||||||
|
|
||||||
|
listTags.append(info.TagInfo(tag_name, tag_url))
|
||||||
|
|
||||||
|
outMsg += f" {tag_name} {tag_url}\r\n"
|
||||||
|
|
||||||
|
#util.DbgOut(outMsg)
|
||||||
|
|
||||||
|
#
|
||||||
|
if "Series" == tds[0]:
|
||||||
|
retPtr.serires = listTags[-1].name
|
||||||
|
elif "Type" == tds[0]:
|
||||||
|
retPtr.type = listTags[-1].name
|
||||||
|
elif "Language" == tds[0]:
|
||||||
|
retPtr.language = listTags[-1].name
|
||||||
|
else:
|
||||||
|
pass
|
||||||
|
|
||||||
|
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()
|
||||||
|
|||||||
214
MgrCalibreDB.py
214
MgrCalibreDB.py
@@ -1,18 +1,12 @@
|
|||||||
import os
|
|
||||||
import sys
|
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
|
||||||
import UtilPack as util
|
import UtilPack as util
|
||||||
|
import MgrSQLiteDB as MyDB
|
||||||
|
|
||||||
class MgrCalibreDB:
|
class MgrCalibreDB(MyDB.MgrSQLiteDB):
|
||||||
def __init__(self, path):
|
def __init__(self, path: str):
|
||||||
self.path = path
|
super().__init__(path)
|
||||||
|
self.init()
|
||||||
def __enter__(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def __exit__(self, ex_type, ex_value, traceback):
|
|
||||||
self.conn.close()
|
|
||||||
|
|
||||||
def init(self):
|
def init(self):
|
||||||
# 데이터베이스 연결 (파일이 없으면 새로 생성됨)
|
# 데이터베이스 연결 (파일이 없으면 새로 생성됨)
|
||||||
@@ -20,97 +14,179 @@ class MgrCalibreDB:
|
|||||||
self.conn.create_function("title_sort", 1, self._title_sort )
|
self.conn.create_function("title_sort", 1, self._title_sort )
|
||||||
self.cursor = self.conn.cursor()
|
self.cursor = self.conn.cursor()
|
||||||
|
|
||||||
|
#
|
||||||
def _title_sort(self, value):
|
def _title_sort(self, value):
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def GetTableAll(self, table):
|
# 테이블의 마지막 인덱스를 가져온다. 잘못되면 -1
|
||||||
if None == self.cursor:
|
def GetLastIndexID(self, strTableName: str) -> int :
|
||||||
return None
|
if True == util.IsEmptyStr(strTableName):
|
||||||
|
return -1
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
if None == table or True == util.IsEmptyStr(table):
|
return listMaxID[0][0] + 1
|
||||||
return None
|
|
||||||
|
#
|
||||||
|
def GetBookTitleByID(self, nID: int) -> str:
|
||||||
|
if 0 >= nID:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
listRet = self.GetValuesOneCondition("books", "title", "id", str(nID))
|
||||||
|
if 0 >= len(listRet):
|
||||||
|
return ""
|
||||||
|
|
||||||
strSQL = f"SELECT * FROM \"{table}\";"
|
return listRet[0]
|
||||||
self.cursor.execute(strSQL)
|
|
||||||
listRet = self.cursor.fetchall()
|
|
||||||
|
#
|
||||||
|
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
|
return listRet
|
||||||
|
|
||||||
def GetTableSchm(self, table):
|
#
|
||||||
if None == self.cursor:
|
def GetDataByBookID(self, nID: int ) -> tuple[str, int, str]:
|
||||||
return None
|
if 0 >= nID:
|
||||||
|
return ("", 0, "")
|
||||||
|
|
||||||
if None == table or True == util.IsEmptyStr(table):
|
strSQL = f"SELECT format, uncompressed_size, name FROM data WHERE book = {nID};"
|
||||||
return None
|
listRet = self.SQLExecute_Get(self.cursor, strSQL)
|
||||||
|
if listRet is None or 0 >= len(listRet):
|
||||||
|
return ("", 0, "")
|
||||||
|
|
||||||
strSQL = f"PRAGMA table_info (\"{table}\");"
|
return listRet[0]
|
||||||
self.cursor.execute(strSQL)
|
|
||||||
listRet = self.cursor.fetchall()
|
#
|
||||||
|
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
|
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:
|
def InsertAuthor(self, strAuthor: str):
|
||||||
return
|
if True == util.IsEmptyStr(strAuthor):
|
||||||
|
|
||||||
if True == util.IsEmptyStr(strAuthor):
|
|
||||||
return
|
return
|
||||||
|
|
||||||
strTableName = "authors"
|
strTableName = "authors"
|
||||||
strSQL = f"SELECT MAX(id) FROM \'{strTableName}\';"
|
nMaxID = self.GetLastIndexID(strTableName)
|
||||||
self.cursor.execute(strSQL)
|
if 0 >= nMaxID:
|
||||||
nMaxID = self.cursor.fetchone()[0]
|
util.DbgOut("Error : Invalid Table Name")
|
||||||
nMaxID += 1.
|
return
|
||||||
|
|
||||||
strAuthorSort = strAuthor.replace(" ", ",")
|
strAuthorSort = strAuthor.replace(" ", ",")
|
||||||
strSQL = f"INSERT OR IGNORE INTO \'{strTableName}\' VALUES ({nMaxID}, \'{strAuthor}\',\'{strAuthorSort}\', \'\');"
|
strSQL = f"INSERT OR IGNORE INTO \'{strTableName}\' VALUES ({nMaxID}, \'{strAuthor}\',\'{strAuthorSort}\', \'\');"
|
||||||
self.cursor.execute(strSQL)
|
self.SQLExecute(self.cursor, strSQL, True)
|
||||||
self.conn.commit()
|
|
||||||
|
|
||||||
def InsertTag(self, tag):
|
|
||||||
if None == self.cursor:
|
|
||||||
return
|
|
||||||
|
|
||||||
if True == util.IsEmptyStr(tag):
|
|
||||||
return
|
|
||||||
|
|
||||||
strTableName = "tags"
|
#
|
||||||
strSQL = f"SELECT MAX(id) FROM \'{strTableName}\';"
|
def InsertTag(self, strTag: str):
|
||||||
self.cursor.execute(strSQL)
|
if True == util.IsEmptyStr(strTag):
|
||||||
nMaxID = self.cursor.fetchone()[0]
|
return
|
||||||
nMaxID += 1.
|
|
||||||
|
|
||||||
strSQL = f"INSERT OR IGNORE INTO \'{strTableName}\' VALUES ({nMaxID}, \'{strAuthor}\', \'\');"
|
strTableName = "tags"
|
||||||
self.cursor.execute(strSQL)
|
nMaxID = self.GetLastIndexID(strTableName)
|
||||||
self.conn.commit()
|
if 0 >= nMaxID:
|
||||||
|
util.DbgOut("Error : Invalid Table Name")
|
||||||
|
return
|
||||||
|
|
||||||
|
strSQL = f"INSERT OR IGNORE INTO \'{strTableName}\' VALUES ({nMaxID}, \'{strTag}\', \'\');"
|
||||||
|
self.SQLExecute(self.cursor, strSQL, True)
|
||||||
|
|
||||||
|
|
||||||
|
""" #Example usage
|
||||||
def main():
|
def main():
|
||||||
db = MgrCalibreDB("/Users/minarinari/캘리버 서재/metadata.db")
|
db = MgrCalibreDB("/Users/minarinari/캘리버 서재/metadata.db")
|
||||||
db.init()
|
db.init()
|
||||||
|
|
||||||
#db.UpdateTableValue("books", 3, "title", "삭막")
|
#db.UpdateTableValue("books", 3, "title", "삭막")
|
||||||
db.InsertAuthor("Oyster")
|
db.InsertAuthor("Oyster")
|
||||||
db.InsertAuthor("Rustle")
|
listValues = db.GetTableAllData("authors")
|
||||||
db.InsertAuthor("ShindoL")
|
|
||||||
|
|
||||||
listValues = db.GetTableAll("authors")
|
|
||||||
print(listValues)
|
print(listValues)
|
||||||
|
|
||||||
|
db.InsertTag("female:Bondage")
|
||||||
|
listValues2 = db.GetTableAllData("Tags")
|
||||||
|
print(listValues2)
|
||||||
|
|
||||||
# For Main Loop
|
# For Main Loop
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
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
|
#cursor.execute('''CREATE TABLE IF NOT EXISTS users
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from PIL import Image
|
|||||||
m_ImgExts = [".jpg",".png",".jpeg",".webp"]
|
m_ImgExts = [".jpg",".png",".jpeg",".webp"]
|
||||||
m_CalLibPath = "/Volumes/NewDataStor/calibre_lib/"
|
m_CalLibPath = "/Volumes/NewDataStor/calibre_lib/"
|
||||||
|
|
||||||
|
|
||||||
def Start():
|
def Start():
|
||||||
pathTrg = os.path.abspath(m_CalLibPath)
|
pathTrg = os.path.abspath(m_CalLibPath)
|
||||||
if False == os.path.exists(pathTrg):
|
if False == os.path.exists(pathTrg):
|
||||||
|
|||||||
490
MgrCalibreUI.py
Normal file
490
MgrCalibreUI.py
Normal 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
216
MgrPupilColDB.py
Normal 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
97
MgrSQLiteDB.py
Normal 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
BIN
Res/layoutImg.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 179 KiB |
259
UI.py
259
UI.py
@@ -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_())
|
|
||||||
|
|
||||||
201
UtilPack.py
201
UtilPack.py
@@ -1,4 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
import rarfile
|
import rarfile
|
||||||
@@ -6,19 +7,22 @@ import zipfile
|
|||||||
import shutil
|
import shutil
|
||||||
import difflib
|
import difflib
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import hashlib
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
m_dbgLevel = 0
|
m_dbgLevel = 0
|
||||||
listDbgStr = []
|
listDbgStr: list[str] = []
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
def IsEmptyStr(string):
|
def IsEmptyStr(string: str) -> bool:
|
||||||
return 0 == len(string.strip())
|
temp = f"{string}"
|
||||||
|
return 0 == len(temp.strip())
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
def GetCurrentTime():
|
def GetCurrentTime() -> str:
|
||||||
# 현재 시간을 구하고 구조체로 변환
|
# 현재 시간을 구하고 구조체로 변환
|
||||||
current_time_struct = time.localtime()
|
current_time_struct = time.localtime()
|
||||||
|
|
||||||
@@ -30,27 +34,40 @@ def GetCurrentTime():
|
|||||||
minute = current_time_struct.tm_min
|
minute = current_time_struct.tm_min
|
||||||
second = current_time_struct.tm_sec
|
second = current_time_struct.tm_sec
|
||||||
|
|
||||||
strRet = (f"{year}/{month}/{day}_{hour}:{minute}:{second}")
|
strRet = (f"{year}-{month}-{day}_{hour}:{minute}:{second}")
|
||||||
|
|
||||||
return strRet
|
return strRet
|
||||||
|
|
||||||
|
|
||||||
#for debug
|
#for debug
|
||||||
def DbgOut(strInput, bPrint = False):
|
def DbgOut(strInput:str, bPrint:bool = False):
|
||||||
strMsg = (f"{GetCurrentTime()} : {strInput}")
|
strMsg = (f"{GetCurrentTime()} : {strInput}")
|
||||||
listDbgStr.append(strMsg)
|
listDbgStr.append(strMsg)
|
||||||
|
|
||||||
if True == bPrint:
|
if True == bPrint:
|
||||||
print(strMsg)
|
print(strMsg)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
def printDbgMessages():
|
def printDbgMessages():
|
||||||
for line in listDbgStr:
|
for line in listDbgStr:
|
||||||
print(line)
|
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):
|
def ListSubDirectories(root_dir:str)-> list[str]:
|
||||||
subdirectories = []
|
subdirectories: list[str] = []
|
||||||
|
|
||||||
# root_dir에서 하위 디렉토리 및 파일 목록을 얻음
|
# root_dir에서 하위 디렉토리 및 파일 목록을 얻음
|
||||||
for dirpath, dirnames, filenames in os.walk(root_dir):
|
for dirpath, dirnames, filenames in os.walk(root_dir):
|
||||||
@@ -64,31 +81,36 @@ def ListSubDirectories(root_dir):
|
|||||||
return subdirectories
|
return subdirectories
|
||||||
|
|
||||||
# 자식 폴더를 구해온다. 직계 자식만
|
# 자식 폴더를 구해온다. 직계 자식만
|
||||||
def ListChildDirectories(pathDir):
|
def ListChildDirectories(pathDir:str, bVisibleOnly: bool = True) -> list[str]:
|
||||||
listRet = []
|
listRet:list[str] = []
|
||||||
for name in os.listdir(pathDir):
|
listTemp = os.listdir(pathDir)
|
||||||
|
for name in listTemp:
|
||||||
pathChild = os.path.join(pathDir, name)
|
pathChild = os.path.join(pathDir, name)
|
||||||
if os.path.isdir(pathChild):
|
if os.path.isdir(pathChild):
|
||||||
listRet.append(pathChild)
|
if True == bVisibleOnly and name.startswith('.'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
listRet.append(name)
|
||||||
|
|
||||||
return listRet
|
return listRet
|
||||||
|
|
||||||
# 파일목록만 구해온다. 자식 폴더에 있는건 무시.
|
# PathDir 에 지정된 폴더의 파일목록만 구해온다. 자식 폴더에 있는건 무시.
|
||||||
def ListContainFiles(pathDir):
|
def ListContainFiles(pathDir:str, bVisibleOnly:bool = True)-> list[str]:
|
||||||
listRet = []
|
listRet:list[str] = []
|
||||||
for name in os.listdir(pathDir):
|
listTemp = os.listdir(pathDir)
|
||||||
|
for name in listTemp:
|
||||||
pathChild = os.path.join(pathDir, name)
|
pathChild = os.path.join(pathDir, name)
|
||||||
if not os.path.isdir(pathChild):
|
if os.path.isfile(pathChild):
|
||||||
listRet.append(pathChild)
|
if True == bVisibleOnly and name.startswith('.'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
listRet.append(name)
|
||||||
|
|
||||||
return listRet
|
return listRet
|
||||||
|
|
||||||
def ListFileExtRcr(pathTrg, listExt):
|
# 리스트에 담긴 확장자와 같은 확장자를 가진 파일을 찾아서 리스트로 반환
|
||||||
listRet= []
|
def ListFileExtRcr(pathTrg: str, listExt: list[str]) -> list[str]:
|
||||||
|
listRet:list[str] = []
|
||||||
if False == isinstance(listExt, list):
|
|
||||||
print("ext must list")
|
|
||||||
return
|
|
||||||
|
|
||||||
# pathTrg의 하위 디렉토리 및 파일 목록을 얻음
|
# pathTrg의 하위 디렉토리 및 파일 목록을 얻음
|
||||||
for dirpath, dirnames, filenames in os.walk(pathTrg):
|
for dirpath, dirnames, filenames in os.walk(pathTrg):
|
||||||
@@ -99,9 +121,8 @@ def ListFileExtRcr(pathTrg, listExt):
|
|||||||
|
|
||||||
return listRet
|
return listRet
|
||||||
|
|
||||||
|
# 입력된 경로에서 부모 폴더를 찾는다. 0 은 자기자신 1은 부모, 숫자대로 위로.
|
||||||
# 입력된 패스에서 부모 폴더를 찾는다. 0 은 자기자신 1은 부모, 숫자대로 위로.
|
def GetParentDirName(FullPath : str, nUp : int)->str:
|
||||||
def GetParentDirName(FullPath, nUp):
|
|
||||||
parts = FullPath.split(os.sep)
|
parts = FullPath.split(os.sep)
|
||||||
|
|
||||||
nTrgIdx = 0
|
nTrgIdx = 0
|
||||||
@@ -114,27 +135,26 @@ def GetParentDirName(FullPath, nUp):
|
|||||||
|
|
||||||
return parts[nTrgIdx]
|
return parts[nTrgIdx]
|
||||||
|
|
||||||
|
|
||||||
# 입력된 경로가 자식 폴더를 가지고 있는지 판단한다.- 최종 폴더인지 여부
|
# 입력된 경로가 자식 폴더를 가지고 있는지 판단한다.- 최종 폴더인지 여부
|
||||||
# 자식이 없으면 True, 자식이 있으면 False
|
# 자식이 없으면 True, 자식이 있으면 False
|
||||||
def IsFinalFolder(path):
|
def IsFinalFolder(path : str) -> bool:
|
||||||
bRet = True
|
bRet = True
|
||||||
|
|
||||||
contents = os.listdir(path)
|
contents = os.listdir(path)
|
||||||
for item in contents:
|
for item in contents:
|
||||||
if True == os.path.isdir(item):
|
if True == os.path.isdir(item):
|
||||||
bRt = False
|
bRet = False
|
||||||
break
|
break
|
||||||
|
|
||||||
return bRet;
|
return bRet;
|
||||||
|
|
||||||
# 어떤 경로 안에서 특정 확장자의 파일을 뽑아내어 그 리스트를 반환한다.
|
# 어떤 경로 안에서 특정 확장자의 파일을 뽑아내어 그 리스트를 반환한다.
|
||||||
def FindFileFromExt(path, ext):
|
def FindFileFromExt(path: str, ext: str)-> list[str]:
|
||||||
bDot = False
|
bDot = False
|
||||||
if 0 <= ext.find('.'):
|
if 0 <= ext.find('.'):
|
||||||
bDot = True
|
bDot = True
|
||||||
|
|
||||||
listRet = []
|
listRet:list[str] = []
|
||||||
if False == os.path.exists(path):
|
if False == os.path.exists(path):
|
||||||
return listRet
|
return listRet
|
||||||
|
|
||||||
@@ -148,9 +168,9 @@ def FindFileFromExt(path, ext):
|
|||||||
listRet.append(item)
|
listRet.append(item)
|
||||||
|
|
||||||
return listRet
|
return listRet
|
||||||
|
|
||||||
# 파일 이름에서 확장자를 뽑아낸다. True : '.' 을 포함한다.
|
# 파일 이름에서 확장자를 뽑아낸다. True : '.' 을 포함한다.
|
||||||
def GetExtStr(file_path, bDot = True):
|
def GetExtStr(file_path: str, bDot: bool = True)-> str:
|
||||||
retStr = ""
|
retStr = ""
|
||||||
# 파일 경로에서 마지막 점을 찾아 확장자를 추출
|
# 파일 경로에서 마지막 점을 찾아 확장자를 추출
|
||||||
last_dot_index = file_path.rfind('.')
|
last_dot_index = file_path.rfind('.')
|
||||||
@@ -165,7 +185,7 @@ def GetExtStr(file_path, bDot = True):
|
|||||||
return retStr
|
return retStr
|
||||||
|
|
||||||
# 문자열에 포함된 단어를 지운다.
|
# 문자열에 포함된 단어를 지운다.
|
||||||
def RmvSubString(mainString, subString):
|
def RmvSubString(mainString: str, subString: str)-> str:
|
||||||
# 문자열에서 부분 문자열의 인덱스를 찾습니다.
|
# 문자열에서 부분 문자열의 인덱스를 찾습니다.
|
||||||
strIdx = mainString.find(subString)
|
strIdx = mainString.find(subString)
|
||||||
if strIdx == -1: # 부분 문자열이 존재하지 않으면 그대로 반환합니다.
|
if strIdx == -1: # 부분 문자열이 존재하지 않으면 그대로 반환합니다.
|
||||||
@@ -176,12 +196,13 @@ def RmvSubString(mainString, subString):
|
|||||||
# 부분 문자열을 제거하고 새로운 문자열을 반환합니다.
|
# 부분 문자열을 제거하고 새로운 문자열을 반환합니다.
|
||||||
return mainString[:strIdx] + mainString[endIdx:]
|
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:
|
with zipfile.ZipFile(zip_file, 'r') as zf:
|
||||||
zf.extractall(extract_to)
|
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:
|
with zipfile.ZipFile(output_zip, 'w') as zf:
|
||||||
for file in files:
|
for file in files:
|
||||||
pathTemp = os.path.join('root', os.path.basename(file))
|
pathTemp = os.path.join('root', os.path.basename(file))
|
||||||
@@ -194,7 +215,7 @@ def CreateZIP(output_zip, *files):
|
|||||||
return bRet
|
return bRet
|
||||||
|
|
||||||
# 파일 리스트에 들어있는 파일만 골라서 압축을 합니다. 상대경로를 제거하는게 기본값.
|
# 파일 리스트에 들어있는 파일만 골라서 압축을 합니다. 상대경로를 제거하는게 기본값.
|
||||||
def CreateZIPShell(zipName, *files, bRmvRPath = True):
|
def CreateZIPShell(zipName: str, files: list[str], bRmvRPath: bool = True) -> bool:
|
||||||
command = "zip "
|
command = "zip "
|
||||||
|
|
||||||
if True == bRmvRPath:
|
if True == bRmvRPath:
|
||||||
@@ -204,17 +225,7 @@ def CreateZIPShell(zipName, *files, bRmvRPath = True):
|
|||||||
|
|
||||||
# 이중 리스트인 이유를 모르겠다.
|
# 이중 리스트인 이유를 모르겠다.
|
||||||
for file in files:
|
for file in files:
|
||||||
strTemp = ""
|
command += f"\"{file}\" "
|
||||||
if isinstance(file, list):
|
|
||||||
strTemp = ' '.join(file)
|
|
||||||
else:
|
|
||||||
strTemp = f"\"{file}\" "
|
|
||||||
|
|
||||||
command += strTemp
|
|
||||||
|
|
||||||
|
|
||||||
# for item in file:
|
|
||||||
# command += f"\"{item}\" "
|
|
||||||
|
|
||||||
result = subprocess.run(command, shell=True, capture_output=True, text=True)
|
result = subprocess.run(command, shell=True, capture_output=True, text=True)
|
||||||
|
|
||||||
@@ -225,7 +236,7 @@ def CreateZIPShell(zipName, *files, bRmvRPath = True):
|
|||||||
return bRet
|
return bRet
|
||||||
|
|
||||||
# 특정 확장자만 쉘을 이용해서 압축한다
|
# 특정 확장자만 쉘을 이용해서 압축한다
|
||||||
def CreateZIPShExt(zipName, TrgExt):
|
def CreateZIPShExt(zipName: str, TrgExt: str)-> bool:
|
||||||
command = f"zip -j {zipName} *.{TrgExt}"
|
command = f"zip -j {zipName} *.{TrgExt}"
|
||||||
|
|
||||||
result = subprocess.run(command, shell=True, capture_output=True, text=True)
|
result = subprocess.run(command, shell=True, capture_output=True, text=True)
|
||||||
@@ -237,8 +248,8 @@ def CreateZIPShExt(zipName, TrgExt):
|
|||||||
return bRet
|
return bRet
|
||||||
|
|
||||||
# 압축 파일 내의 모든 파일 및 디렉토리 목록 가져오기
|
# 압축 파일 내의 모든 파일 및 디렉토리 목록 가져오기
|
||||||
def GetZipContentList(path):
|
def GetZipContentList(path: str) -> list[str]:
|
||||||
if None == path or not os.path.isfile(path):
|
if True == IsEmptyStr(path) or not os.path.isfile(path):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
listRet = []
|
listRet = []
|
||||||
@@ -247,23 +258,19 @@ def GetZipContentList(path):
|
|||||||
|
|
||||||
return listRet
|
return listRet
|
||||||
|
|
||||||
def GetZippedFileByte(pathZip, FileName):
|
#
|
||||||
if None == pathZip or not os.path.isfile(pathZip):
|
def GetZippedFileByte(pathZip: str, FileName: str) -> bytes:
|
||||||
return None
|
retBytes:bytes = bytes()
|
||||||
|
if True == os.path.isfile(pathZip):
|
||||||
retBytes = None
|
with zipfile.ZipFile( pathZip , 'r') as zip_file:
|
||||||
with zipfile.ZipFile( pathZip , 'r') as zip_file:
|
# 압축 파일 내의 특정 파일을 읽기
|
||||||
if not FileName in zip_file.namelist():
|
with zip_file.open(FileName) as file:
|
||||||
return None
|
retBytes = file.read()
|
||||||
|
|
||||||
with zip_file.open(FileName) as file:
|
|
||||||
retBytes = file.read()
|
|
||||||
|
|
||||||
return retBytes
|
return retBytes
|
||||||
|
|
||||||
|
|
||||||
# JSON 을 트리 구조로 출력한다.
|
# JSON 을 트리 구조로 출력한다.
|
||||||
def PrintJSONTree(data, indent=0):
|
def PrintJSONTree(data, indent: int=0 ) -> None:
|
||||||
if isinstance(data, dict):
|
if isinstance(data, dict):
|
||||||
for key, value in data.items():
|
for key, value in data.items():
|
||||||
print(' ' * indent + str(key))
|
print(' ' * indent + str(key))
|
||||||
@@ -274,6 +281,7 @@ def PrintJSONTree(data, indent=0):
|
|||||||
else:
|
else:
|
||||||
print(' ' * indent + str(data))
|
print(' ' * indent + str(data))
|
||||||
|
|
||||||
|
#
|
||||||
def IsPathWithin(base_path: str, target_path: str) -> bool:
|
def IsPathWithin(base_path: str, target_path: str) -> bool:
|
||||||
base = Path(base_path).resolve()
|
base = Path(base_path).resolve()
|
||||||
target = Path(target_path).resolve()
|
target = Path(target_path).resolve()
|
||||||
@@ -285,7 +293,54 @@ def UUIDGenRandom():
|
|||||||
return random_uuid
|
return random_uuid
|
||||||
|
|
||||||
# 이름을 기반으로 UUID 생성
|
# 이름을 기반으로 UUID 생성
|
||||||
def UUIDGenName(SeedName):
|
def UUIDGenName(SeedName:str):
|
||||||
namespace_uuid = uuid.uuid5(uuid.NAMESPACE_DNS, SeedName)
|
namespace_uuid = uuid.uuid5(uuid.NAMESPACE_DNS, SeedName)
|
||||||
return namespace_uuid
|
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("파일이 손상되었거나 다릅니다!")
|
||||||
|
"""
|
||||||
78
main.py
78
main.py
@@ -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 os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
import MgrCalibreUI
|
||||||
|
|
||||||
|
from PyQt5.QtWidgets import QApplication
|
||||||
|
|
||||||
|
|
||||||
def main(argc, argv):
|
def main(argc, argv):
|
||||||
if argc != 2:
|
app = QApplication(argv)
|
||||||
printUsage()
|
|
||||||
return
|
|
||||||
|
|
||||||
trgPath = argv[1]
|
|
||||||
#getHitomi.GetSearchResult("2890685")
|
|
||||||
#etEhentai.GetSearchResult("artist%3A%22kotomi+yo-ji%24%22")
|
|
||||||
|
|
||||||
#mgrCal.Start()
|
app.setQuitOnLastWindowClosed(True)
|
||||||
#util.printDbgMessages()
|
main_window = MgrCalibreUI.MyApp()
|
||||||
|
main_window.show()
|
||||||
|
|
||||||
#artist:"kotomi yo-ji$"
|
sys.exit(app.exec_())
|
||||||
#"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
|
# For Main Loop
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
@@ -76,3 +22,5 @@ if __name__ == '__main__':
|
|||||||
argv = sys.argv
|
argv = sys.argv
|
||||||
main(argc, argv)
|
main(argc, argv)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
93
pupildata.py
93
pupildata.py
@@ -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
|
|
||||||
|
|
||||||
181
rarzipcbz.py
181
rarzipcbz.py
@@ -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)
|
|
||||||
@@ -1,9 +1,14 @@
|
|||||||
beautifulsoup4==4.12.3
|
beautifulsoup4>=4.12.3
|
||||||
bs4==0.0.2
|
image>=1.5.33
|
||||||
PyQt5==5.15.11
|
jedi>=0.19.1
|
||||||
PyQt5-Qt5==5.15.2
|
numpy>=1.26.4
|
||||||
PyQt5_sip==12.15.0
|
openpyxl>=3.1.2
|
||||||
rarfile==4.2
|
pandas>=2.2.2
|
||||||
setuptools==75.6.0
|
PyQt5>=5.15.11
|
||||||
soupsieve==2.6
|
PyQt6>=6.7.0
|
||||||
wheel==0.45.1
|
python-dateutil>=2.9.0.post0
|
||||||
|
pytz>=2024.1
|
||||||
|
pyzmq>=25.1.2
|
||||||
|
rarfile>=4.2
|
||||||
|
selenium>=4.19.0
|
||||||
|
sqlparse>=0.5.0
|
||||||
|
|||||||
Reference in New Issue
Block a user