diff --git a/.gitignore b/.gitignore index 36f38b8..aea3607 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ venv_mangainfo/ -mangaDB.xlsx -temp.db -temp.json -untitled0.py +Data/ +Temp/ +__pycache__/ diff --git a/DataClass.py b/DataClass.py index 232b37a..321d7e7 100644 --- a/DataClass.py +++ b/DataClass.py @@ -1,81 +1,8 @@ -class CBZInfo: - def __init__(self, title, url): - self.title = title - self.url = url - self.series = "" - 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): - 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.filename}\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.SaveToTextArtists() + f"\r\n" - retText += self.SavetToTextRelatedGalleryID() + f"\r\n" - retText += self.SavetToTextTags() + f"\r\n" - retText += "\}" - - def SaveToTextTagList(self, listTags): - RetText = "\{" - for tag in listTags: - RetText += (f"\"{tag.name}\" ") - - RetText += "\}" - - return RetText - - def SaveToTextArtists(self): - retText = f"Artists : " - retText += self.SaveToTextTagList(self.artists) - - return retText - - def SavetToTextRelatedGalleryID(self): - retText = f"Gallery_ID : " - retText += self.SaveToTextTagList(self.related_galID) - - return retText - - def SavetToTextTags(self): - retText = f"Tags : " - retText += self.SaveToTextTagList(self.tags) - - return retText - - def AddTag(self, name): - self.tags.add(name) - - def RmvTag(self, name): - self.tags.discard(name) - - def AddArtist(self, name): - self.artists.add(name) - - def RmvArtist(self, name): - self.artists.discard(name) - +from typing import Set +import UtilPack as util class TagInfo: - def __init__(self, name, url): + def __init__(self, name: str, url: str): self.name = name self.url = url @@ -84,11 +11,92 @@ class TagInfo: class ImageFileInfo: - def __init__(self, name, height, width, hashValue, bWebp): - self.name = name + def __init__(self, path: str, height: int, width: int, hashValue: str, bWebp: bool): + self.path = path self.height = height self.width = width self.hashValue = hashValue self.bWebp = bWebp + + def CehckHash(self) -> bool: + return util.VerifyIsValidFile(self.path, self.hashValue) + + +class CBZInfo: + def __init__(self, title: str, url: str): + self.title = title + self.url = url + self.series = "" + self.type = "" + self.Arcfilename = "" + self.torrent = "" + self.language = "" + self.gallery_id = 0 + # 중복을 허용하지 않는 집합으로 초기화 + self.related_galID = set[str]() + self.artists = set[str]() + self.tags = set[str]() + self.files = list[ImageFileInfo]() + + def __str__(self): + strArtists = ", ".join(self.artists) + strTags = ", ".join(self.tags) + + return f"ID : {self.gallery_id} - {self.title} by {strArtists} - #{strTags}" + + def SaveToText(self): + retText = "{" + retText += f"Title : {self.title}\r\n" + retText += f"URL : {self.url}\r\n" + retText += f"Series : {self.series}\r\n" + retText += f"Type : {self.type}\r\n" + retText += f"Filename : {self.Arcfilename}\r\n" + retText += f"Torrent : {self.torrent}\r\n" + retText += f"Language : {self.language}\r\n" + retText += f"Gallery ID : {self.gallery_id}\r\n" + retText += self.ArtistsSaveToText() + f"\r\n" + retText += self.RelatedGalleryIDsSaveToText() + f"\r\n" + retText += self.TagsSaveToText() + f"\r\n" + retText += "}" + + def TagListSaveToText(self, setTags: Set[str])-> str: + RetText = "{" + for tag in setTags: + RetText += (f"\"{tag}\" ") + + RetText += "}" + + return RetText + + def ArtistsSaveToText(self)-> str: + retText = f"Artists : " + retText += self.TagListSaveToText(self.artists) + + return retText + + def RelatedGalleryIDsSaveToText(self)-> str: + retText = f"Gallery_ID : " + retText += self.TagListSaveToText(self.related_galID) + + return retText + + def TagsSaveToText(self)-> str: + retText = f"Tags : " + retText += self.TagListSaveToText(self.tags) + + return retText + + def AddTag(self, strTag: str): + self.tags.add(strTag) + + def RmvTag(self, strTag: str): + self.tags.discard(strTag) + + def AddArtist(self, strName: str): + self.artists.add(strName) + + def RmvArtist(self, strName: str): + self.artists.discard(strName) + \ No newline at end of file diff --git a/pupildata.py b/DataClass_Pupil.py similarity index 51% rename from pupildata.py rename to DataClass_Pupil.py index ab6249b..bd5dda1 100644 --- a/pupildata.py +++ b/DataClass_Pupil.py @@ -18,37 +18,97 @@ JTITLE = "japanese_title" # pupil : Json # Caribre : text -# My : CSV -class PupuilInfoFile: +# Me : CSV +class PupilData: m_data = None def __init__(self, argv): self.argv = argv - + self.PupilJSONOpen(argv) + def __enter__(self): - self.PupilJSONOpen(self.argv) + pass def __exit__(self, ex_type, ex_value, traceback): pass - def PupilJSONOpen(self, argv): - if True == util.IsEmptyStr(argv): - print("PupilData: input Null") + def PupilJSONOpen(self, path:str) -> None: + self.m_data = None + + if True == util.IsEmptyStr(path): + util.DbgOut("PupilData: input Null", True) return - if True == os.path.exists(argv): - print("pupildata : file") - with open(argv, "r", encoding="utf-8") as file: + if False == os.path.exists(path): + return + + try: + with open(path, "r", encoding="utf-8") as file: self.m_data = json.load(file) - else: - print("pupildata : text") - self.m_data = json.loads(argv) + 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 GetInfo(self): - if None == self.m_data and None != self.argv: - self.m_data = self.PupilJSONOpen(self.argv) + """ + 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 @@ -86,9 +146,33 @@ class PupuilInfoFile: 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 GetImageFilesInfo(self): + def GetImgFileList(self): if None == self.m_data: return None diff --git a/GetArc_Hitomi.py b/GetArc_Hitomi.py index 8c3f3d2..0c3258b 100644 --- a/GetArc_Hitomi.py +++ b/GetArc_Hitomi.py @@ -6,180 +6,209 @@ from selenium.common.exceptions import TimeoutException from bs4 import BeautifulSoup import time - import UtilPack as util import DataClass as info - # -def GetSearchResult(searchWord): - url = getSiteUrl(searchWord) +class GetArc_Hitomi: + m_strBaseURL = "https://hitomi.la/" + + 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, True) - - driver = webdriver.Chrome() - driver.get(url) - - # 웹페이지가 로드될 때까지 기다리기 - 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 - - listRet = parseMangaInfos(strContent) - - for Idx in range(len(listRet)): - print(f"{Idx} : {listRet[Idx]}") - - driver.quit() - -def GetListSearchResult(list_ID): - driver = webdriver.Chrome() - - # 웹페이지가 로드될 때까지 기다리기 - try: - for id in list_ID: - url = getSiteUrl(id) - util.DbgOut("Hitomi : " + url, True) - - driver.get(url) - - WebDriverWait(driver, 10).until( - lambda d: d.execute_script("return document.readyState") == "complete" - ) - - time.sleep(2) - - strContent = driver.page_source - listRet = 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") - file.close() - except IOError: - util.DbgOut(f"Error: Could not write to the file at {id}.txt.", True) - - except Exception as e: - util.DbgOut("Hitomi Loading Error : ", e) - finally: - driver.quit() - - -def getSiteUrl(searchWord): - strRet = "https://hitomi.la/" - - if False == searchWord.isdigit(): - strRet = f"{strRet}search.html?{searchWord}" - else: - strRet = f"{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) - - return listDJs - - -def djParse(soup_element): - childs = soup_element.find_all(class_='dj') - - listInfos = [] - for child in childs: - info = djTitleParse(child) - - listTag1 = djArtistParse(child, info) - listTag2 = djDescParse(child, info) - - listInfos.append(info) - - return listInfos - - -def djTitleParse(input_element): - element = input_element.find('h1', class_='lillie') - 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 - - -def djDescParse(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 + strURL = "" + if strWord.isdigit(): + strURL = self.getSiteUrlForGallery(int(strWord)) else: - pass - - return listTags + strURL = self.getSiteUrlForSearch(strWord) + + util.DbgOut(f"Hitomi : {strURL}", True) + driver = webdriver.Chrome() + driver.get(strURL) + + # 웹페이지가 로드될 때까지 기다리기 + 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() + + 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) + + listRet = self.parseMangaInfos(strContent) + + 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 getSiteUrlForSearch(self, searchWord: str) -> str: + return f"{self.m_strBaseURL}search.html?{searchWord}" + + def getSiteUrlForGallery(self, nHitomiID: int) -> str: + return f"{self.m_strBaseURL}galleries/{nHitomiID}.html" + + # + def parseMangaInfos(self, html_doc : str) -> list[info.CBZInfo]: + # BeautifulSoup 객체 생성 + soup = BeautifulSoup(html_doc, 'html.parser') + gallery_elements = soup.find_all(class_='gallery-content') + + listDJs: list[info.CBZInfo] = [] + for element in gallery_elements: + listDJ = self.djParse(element) + listDJs.extend(listDJ) + + return listDJs + + # + def djParse(self, soup_element) -> list[info.CBZInfo]: + childs = soup_element.find_all(class_='dj') + + 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 + + a_tag = element.find('a') + strURL: str = a_tag.get('href') + + #util.DbgOut("title : " + title) + #util.DbgOut("URl : " + url) + + return info.CBZInfo(strTitle, strURL) + + # + def djArtistParse(self, input_element, retPtr): + element = input_element.find('div', class_='artist-list') + + a_tags = element.find_all('a') + for tag in a_tags: + artist = tag.text + a_url = tag.get('href') + retPtr.AddArtist(artist) + + # + 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() diff --git a/MgrCalibreDB.py b/MgrCalibreDB.py index a1ff94e..275ebad 100644 --- a/MgrCalibreDB.py +++ b/MgrCalibreDB.py @@ -1,134 +1,190 @@ -import os -import sys import sqlite3 import UtilPack as util +import MgrSQLiteDB as MyDB -class MgrCalibreDB: - def __init__(self, path): - self.path = path - - def __enter__(self): - pass - - def __exit__(self, ex_type, ex_value, traceback): - self.conn.close() - +class MgrCalibreDB(MyDB.MgrSQLiteDB): def init(self): + print(f"{self.path} DB Calibre Init") + # 데이터베이스 연결 (파일이 없으면 새로 생성됨) self.conn = sqlite3.connect(self.path) self.conn.create_function("title_sort", 1, self._title_sort ) self.cursor = self.conn.cursor() + # def _title_sort(self, value): return 0 - - def GetTableAll(self, table): - if None == self.cursor: - return None - - if None == table or True == util.IsEmptyStr(table): - return None - - strSQL = f"SELECT * FROM \"{table}\";" - self.cursor.execute(strSQL) - listRet = self.cursor.fetchall() - - return listRet - def GetTableSchm(self, table): - if None == self.cursor: - return None - - if None == table or True == util.IsEmptyStr(table): - return None - - strSQL = f"PRAGMA table_info (\"{table}\");" - self.cursor.execute(strSQL) - listRet = self.cursor.fetchall() - return listRet - - def GetBookListforUI_ArcList(self): - if None == self.cursor: - return None - - try: - strSQL = f"Select title, path from books;" - self.cursor.execute(strSQL) - listRet = self.cursor.fetchall() - except sqlite3.Error as e: - listRet = [] - print(f"SQLite Erro occurred: {e}") + # 테이블의 마지막 인덱스를 가져온다. 잘못되면 -1 + def GetLastIndexID(self, strTableName: str) -> int : + if True == util.IsEmptyStr(strTableName): + return -1 - return listRet + strSQL = f"SELECT MAX(id) FROM \'{strTableName}\';" + listMaxID = self.SQLExecute_Get(self.cursor, strSQL) + if listMaxID is None or 0 >= len(listMaxID): + util.DbgOut("Error : Invalid Table Name") + return -1 + return listMaxID[0][0] + 1 - def UpdateTableValue(self,Table, nID, ColName, Value): - if None == self.cursor: - return + # + def GetBookTitleByID(self, nID: int) -> str: + if 0 >= nID: + return "" + + listRet = self.GetValuesOneCondition("books", "title", "id", str(nID)) + if 0 >= len(listRet): + return "" + + return listRet[0] + + + # + def GetAuthorByID(self, nID: int ) -> str: + if 0 >= nID: + return "" + + listRet = self.GetValuesOneCondition("authors", "name", "id", f"{nID}") + if 0 >= len(listRet): + return "" + + return listRet[0] + + + # + def GetAuthorsByBookID(self, nID: int ) -> list[str]: + if 0 >= nID: + return [] + + listAuthorID = self.GetValuesOneCondition("books_authors_link", "author", "book", f"{nID}") + print(f"GetAuthorsByBookID : {listAuthorID}") + + if 0 >= len(listAuthorID): + return [] + + listRet = list[str]() + for strAuthorID in listAuthorID: + if True == util.IsEmptyStr(strAuthorID): + continue - if 0 >= nID or True == util.IsEmptyStr(Table) or True == util.IsEmptyStr(ColName): - return - - strSQL = f"UPDATE {Table} SET \'{ColName}\' = \'{Value}\' WHERE id = {nID};" - self.cursor.execute(strSQL) - self.conn.commit() - - def InsertAuthor(self, strAuthor): - if None == self.cursor: - return + nAuthorID = int(strAuthorID) + strAuthorName = self.GetAuthorByID(nAuthorID) + if True == util.IsEmptyStr(strAuthorName): + continue - if True == util.IsEmptyStr(strAuthor): + listRet.append(strAuthorName) + + return listRet + + # + def GetDataByBookID(self, nID: int ) -> tuple[str, int, str]: + if 0 >= nID: + return ("", 0, "") + + strSQL = f"SELECT format, uncompressed_size, name FROM data WHERE book = {nID};" + listRet = self.SQLExecute_Get(self.cursor, strSQL) + if listRet is None or 0 >= len(listRet): + return ("", 0, "") + + return listRet[0] + + # + def GetTagByID(self, strTag: str) -> int: + if True == util.IsEmptyStr(strTag): + return -1 + + listRet = self.GetValuesOneCondition("tags", "id", "name", strTag) + if 0 >= len(listRet): + return -1 + + return int(listRet[0]) + + + # title, author, cover, ext(path), id + # id, title, has_cover, path, And author + def GetBookListforUI_ArcList(self) -> list[tuple[str, str, str, str, str]]: + strSQL = f"Select id, title, path, has_cover from books;" + listResult = self.SQLExecute_Get(self.cursor, strSQL) + if listResult is None or 0 >= len(listResult): + return [] + + listRet = list[tuple[str, str, str, str, str]]() + for item in listResult: + nID = item[0] + strTitle = item[1] + strPath = item[2] + nHasCover = item[3] + + if 0 >= nID: + continue + + listAuthors = self.GetAuthorsByBookID(nID) + strAuthor = ", ".join(listAuthors) if listAuthors else "Unknown" + + listRet.append((strTitle, strAuthor, str(nHasCover), strPath, str(nID))) + + return listRet + + # + def InsertAuthor(self, strAuthor: str): + if True == util.IsEmptyStr(strAuthor): return - strTableName = "authors" - strSQL = f"SELECT MAX(id) FROM \'{strTableName}\';" - self.cursor.execute(strSQL) - nMaxID = self.cursor.fetchone()[0] - nMaxID += 1 + strTableName = "authors" + nMaxID = self.GetLastIndexID(strTableName) + if 0 >= nMaxID: + util.DbgOut("Error : Invalid Table Name") + return strAuthorSort = strAuthor.replace(" ", ",") strSQL = f"INSERT OR IGNORE INTO \'{strTableName}\' VALUES ({nMaxID}, \'{strAuthor}\',\'{strAuthorSort}\', \'\');" - self.cursor.execute(strSQL) - self.conn.commit() + self.SQLExecute(self.cursor, strSQL, True) - def InsertTag(self, tag): - if None == self.cursor: - return - - if True == util.IsEmptyStr(tag): + + # + def InsertTag(self, strTag: str): + if True == util.IsEmptyStr(strTag): return strTableName = "tags" - strSQL = f"SELECT MAX(id) FROM \'{strTableName}\';" - self.cursor.execute(strSQL) - nMaxID = self.cursor.fetchone()[0] - nMaxID += 1. + nMaxID = self.GetLastIndexID(strTableName) + if 0 >= nMaxID: + util.DbgOut("Error : Invalid Table Name") + return - strSQL = f"INSERT OR IGNORE INTO \'{strTableName}\' VALUES ({nMaxID}, \'{strAuthor}\', \'\');" - self.cursor.execute(strSQL) - self.conn.commit() + strSQL = f"INSERT OR IGNORE INTO \'{strTableName}\' VALUES ({nMaxID}, \'{strTag}\', \'\');" + self.SQLExecute(self.cursor, strSQL, True) -""" +""" #Example usage def main(): db = MgrCalibreDB("/Users/minarinari/캘리버 서재/metadata.db") db.init() #db.UpdateTableValue("books", 3, "title", "삭막") db.InsertAuthor("Oyster") - db.InsertAuthor("Rustle") - db.InsertAuthor("ShindoL") - - listValues = db.GetTableAll("authors") + listValues = db.GetTableAllData("authors") print(listValues) + db.InsertTag("female:Bondage") + listValues2 = db.GetTableAllData("Tags") + print(listValues2) + # For Main Loop if __name__ == '__main__': main() """ +#coments table +# 자체 id, book id, 코멘트...<- 여기다 json 을 넣자. +# id, book ,comment + +# data table +# 자체 id, book id, 파일 포멧, cbz, 크기, 파일 이름 +# id, book, format, uncompressed_size, name # 테이블 생성 #cursor.execute('''CREATE TABLE IF NOT EXISTS users diff --git a/MgrCalibreLibs.py b/MgrCalibreLibs.py index 0ffebfd..21079f1 100644 --- a/MgrCalibreLibs.py +++ b/MgrCalibreLibs.py @@ -11,6 +11,7 @@ from PIL import Image m_ImgExts = [".jpg",".png",".jpeg",".webp"] m_CalLibPath = "/Volumes/NewDataStor/calibre_lib/" + def Start(): pathTrg = os.path.abspath(m_CalLibPath) if False == os.path.exists(pathTrg): diff --git a/MgrCalibreUI.py b/MgrCalibreUI.py new file mode 100644 index 0000000..2aa58a0 --- /dev/null +++ b/MgrCalibreUI.py @@ -0,0 +1,467 @@ +import sys +import os +import shutil +import UtilPack as util +import DataClass_Pupil as pupil +import MgrCalibreDB +import MgrPupilColDB +#import MgrCalibreLibs as calLib + + +from PyQt5.QtCore import Qt, QSettings, QRect +from PyQt5.QtWidgets import QApplication, QWidget, QMainWindow, QPushButton, QVBoxLayout, QLineEdit, \ + QHBoxLayout, QTableWidget, QTableWidgetItem, QAbstractItemView, QHeaderView, QFileDialog, \ + QListWidget, QListWidgetItem, QMessageBox +from PyQt5.QtGui import QResizeEvent, QCloseEvent, QColor + +#QApplication.setAttribute(Qt.AA_ShareOpenGLContexts) + +class MyApp(QMainWindow): + m_DictDuplicate: dict[int, list[str]] = {} + m_ListIncomplMngas: list[str] = [] + + def __init__(self): + super().__init__() + self.loadINI() + self.initDB() + self.initUI() + + # + def closeEvent(self, a0: 'QCloseEvent | None'): + filenameLog = f"{util.GetCurrentTime()}_DbgLog.txt" + pathDbgLog = os.path.join( self.pathLog, filenameLog) + util.SaveDbgMessages(pathDbgLog) + self.saveINI() + super().closeEvent(a0) + + # + def resizeEvent(self, a0: 'QResizeEvent | None'): + PScreen = QApplication.primaryScreen() + rcDesktop: QRect = QRect(0, 0, 800, 600) + if PScreen is not None: + rcDesktop = PScreen.availableGeometry() + + rcWnd = self.geometry() + if rcWnd.width() > rcDesktop.width(): + rcWnd.setWidth(rcDesktop.width()) + + print(f"Resize Event: {rcWnd.width()}x{rcWnd.height()} at {rcWnd.x()},{rcWnd.y()}") + + super().resizeEvent(a0) + + # + def loadINI(self): + settings = QSettings('MyApp', 'settings') + + self.pathLog = settings.value('LoggingPath', "~/Workspace/Log", type=str) + if False == os.path.exists(self.pathLog): + os.makedirs(self.pathLog, exist_ok=True) + + self.pathDB = settings.value('PupilDBPath', "~/Workspace/DB", type=str) + if False == os.path.exists(self.pathDB): + os.makedirs(self.pathDB, exist_ok=True) + + self.pathLastCalibre = settings.value('LastCalibrePath', "~/캘리버 서재", type=str) + if False == os.path.exists(self.pathDB): + os.makedirs(self.pathDB, exist_ok=True) + + self.fileNamePupilDB = "MyMangaData.db" + self.fileNameCallibreDB = "metadata.db" + + window_size = settings.value('window/size', '800, 600') + window_position = settings.value('window/position', '100, 100') + + ItemCnt = settings.value('folders/count', 0, type=int) + for idx in range(ItemCnt): + item_text = settings.value(f'folders/{idx}', '', type=str) + if item_text: + self.listWidget_Folders.addItem(item_text) + + # + def saveINI(self): + settings = QSettings('MyApp', 'settings') + + # 키-값 형식으로 데이터 저장 + settings.setValue('window/size', "1024,768") + settings.setValue('window/position', "100,100") + + # 폴더 목록 저장 + ListItems: list[QListWidgetItem] = self.listWidget_Folders.items(None) + for idx in range(len(ListItems)): + item = ListItems[idx] + settings.setValue(f'folders/{idx}', item.text()) + + settings.setValue('folders/count', self.listWidget_Folders.count()) + + # + def initDB(self): + pathPupilDB = os.path.join(self.pathDB, self.fileNamePupilDB) + util.DbgOut(f"Loading Pupil DB from {pathPupilDB}", True) + self.DBPupil = MgrPupilColDB.MgrPupilColDB(pathPupilDB) + + pathCalibreDB = os.path.join(self.pathLastCalibre, self.fileNameCallibreDB) + util.DbgOut(f"Loading Calibre DB from {pathCalibreDB}", True) + self.DBCalibre = MgrCalibreDB.MgrCalibreDB(pathCalibreDB) + + # + def MakeUI_Left(self): + layout = QVBoxLayout() + + layout_top = QHBoxLayout() + self.listWidget_Folders = QListWidget() + self.listWidget_Folders.setFixedHeight(100) + + layout_Btns = QVBoxLayout() + btn_FolderAdd = QPushButton("폴더 추가") + btn_FolderAdd.clicked.connect(self.on_btnFolderAdd_clicked) + btn_FolderDel = QPushButton("폴더 삭제") + btn_FolderDel.clicked.connect(self.on_btnFolderDel_clicked) + btn_FolderParss = QPushButton("폴더 파싱") + btn_FolderParss.clicked.connect(self.on_btnFolderParse_clicked) + + layout_Btns.addWidget(btn_FolderAdd) + layout_Btns.addWidget(btn_FolderDel) + layout_Btns.addWidget(btn_FolderParss) + + layout_top.addWidget(self.listWidget_Folders) + layout_top.addLayout(layout_Btns) + + self.tableWidget_Src = QTableWidget() + self.tableWidget_Src.verticalHeader().setVisible(False) + self.tableWidget_Src.setEditTriggers(QAbstractItemView.NoEditTriggers) + self.tableWidget_Src.setSortingEnabled(True) + self.tableWidget_Src.setColumnCount(5) + self.tableWidget_Src.setHorizontalHeaderLabels(["Path", "Title", "H.ID", "Img Cnt", "Dw Cnt"]) + self.tableWidget_Src.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) + self.tableWidget_Src.horizontalHeader().sectionClicked.connect(self.on_tableWidget_Src_headerClicked) + self.tableWidget_Src.setSelectionBehavior(QAbstractItemView.SelectRows) + self.tableWidget_Src.setSelectionMode(QAbstractItemView.SingleSelection) + self.tableWidget_Src.itemSelectionChanged.connect(self.on_tableWidget_Src_itemSelectionChanged) + + layout.addLayout(layout_top) + layout.addWidget(self.tableWidget_Src) + + return layout + + # + def MakeUI_Center(self): + layout = QVBoxLayout() + + btn_Emptyfolder = QPushButton("빈 폴더 이동") + btn_Emptyfolder.clicked.connect(self.on_btn_Emptyfolder_clicked) + btn_ChkDuplicate = QPushButton("중복 검사 및 제거") + btn_ChkDuplicate.clicked.connect(self.on_btn_ChkDuplicate_clicked) + btn_Archive = QPushButton("압축 및 데이터 저장") + btn_Archive.clicked.connect(self.on_btn_Archive_clicked) + btn_EnterCalibre = QPushButton("컬리버에 삽입") + btn_EnterCalibre.clicked.connect(self.on_btn_EnterCalibre_clicked) + + layout.addWidget(btn_Emptyfolder) + layout.addWidget(btn_ChkDuplicate) + layout.addWidget(btn_Archive) + layout.addWidget(btn_EnterCalibre) + + return layout + + # + def MakeUI_Right(self): + layout_top = QHBoxLayout() + self.edit_DB = QLineEdit(self) + self.edit_DB.setReadOnly(True) + self.edit_DB.setText(self.pathLastCalibre) + btn_DB = QPushButton("...") + btn_DB.setFixedWidth(50) + btn_DB.clicked.connect(self.on_btnDB_clicked) + layout_top.addWidget(self.edit_DB) + layout_top.addWidget(btn_DB) + + self.tableWidget_DB = QTableWidget() + self.tableWidget_DB.verticalHeader().setVisible(False) + self.tableWidget_DB.setColumnCount(5) + self.tableWidget_DB.setHorizontalHeaderLabels(["Title", "Author", "Cover", "Exts", "ID"]) + self.tableWidget_DB.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) + self.tableWidget_DB.setSelectionBehavior(QAbstractItemView.SelectRows) + self.tableWidget_DB.setSelectionMode(QAbstractItemView.SingleSelection) + self.tableWidget_DB.itemSelectionChanged.connect(self.on_tableWidget_DB_itemSelectionChanged) + + layout = QVBoxLayout() + layout.addLayout(layout_top) + layout.addWidget(self.tableWidget_DB) + + return layout + + # + def MakeUI(self): + layout_L = self.MakeUI_Left() + layout_C = self.MakeUI_Center() + layout_R = self.MakeUI_Right() + + # 레이아웃 설정 + layout = QHBoxLayout() + layout.addLayout(layout_L, stretch = 10) + layout.addLayout(layout_C, stretch = 1) + layout.addLayout(layout_R, stretch = 10) + + return layout + + # + def initUI(self): + layout = self.MakeUI() + + # 레이아웃을 윈도우에 적용 + central_widget = QWidget() + central_widget.setLayout(layout) + self.setCentralWidget(central_widget) + self.setWindowTitle('Manga Database') + self.resize(800, 600) + self.show() + + # 테이블의 특정 행에 배경색을 설정한다 + # nRow: 배경색을 설정할 행 번호, color: 배경색 (Qt.GlobalColor) + def SrcTableRowBgColor(self, nRow:int, color:Qt.GlobalColor) -> None: + for col in range(self.tableWidget_Src.columnCount()): + item = self.tableWidget_Src.item(nRow, col) + if item: + item.setBackground(Qt.GlobalColor(color)) + + # + def LoadSrcFolder(self, path:str)-> None: + # 폴더가 없으면 리턴 + if True == util.IsEmptyStr(path) or False == os.path.exists(path): + return + + # 폴더의 내용물을 읽어온다. + listFiles = util.ListChildDirectories(path) + util.DbgOut(f"Src Count : {len(listFiles)}", True) + + # 테이블 초기화 + nLastRow = self.tableWidget_Src.rowCount() + self.tableWidget_Src.setRowCount(nLastRow + len(listFiles)) + + nRow = nLastRow + # 테이블에 데이터 추가 + for item in listFiles: + pathDir = os.path.join(path, item) + if False == os.path.isdir(pathDir): + util.DbgOut(f"Not a directory: {pathDir}", True) + continue + + item_0 = QTableWidgetItem(pathDir) + self.tableWidget_Src.setItem(nRow, 0, item_0) + + FullPath = os.path.join(pathDir, ".metadata") + data = pupil.PupilData(FullPath) + util.DbgOut(f"Loaded MetaData Path : {FullPath}", True) + + item_1 = QTableWidgetItem(data.GetTitle()) + self.tableWidget_Src.setItem(nRow, 1, item_1) + + strID = data.GetHitomiID() + if True == util.IsEmptyStr(strID): + strID = util.GetTextInBrakets(item)[0] + + item_2 = QTableWidgetItem(strID) + self.tableWidget_Src.setItem(nRow, 2, item_2) + + nImgListLen = data.GetImgFileCount() + item_3 = QTableWidgetItem(f"{nImgListLen}") + self.tableWidget_Src.setItem(nRow, 3, item_3) + + nImgFileCnt = len(util.ListContainFiles(pathDir)) + item_4 = QTableWidgetItem(f"{nImgFileCnt}") + self.tableWidget_Src.setItem(nRow, 4, item_4) + + # 중복 검사 를 위해 경로를 딕셔너리에 저장 + nID = int(strID) + if nID not in self.m_DictDuplicate: + self.m_DictDuplicate[nID] = [] + self.m_DictDuplicate[nID].append(pathDir) + + # 중복된 만화가 있으면 배경색을 변경 + if len(self.m_DictDuplicate[nID]) > 1 : + self.SrcTableRowBgColor(nRow, Qt.GlobalColor.green) + + # JSon 데이터의 파일 개수와 실제 다운받은 파일 개수가 다르면 리스트에 저장, 표시한다. + if 0 >= nImgFileCnt or 0 >= nImgListLen or nImgFileCnt != nImgListLen: + self.m_ListNotCompleteMngas.append(pathDir) + self.SrcTableRowBgColor(nRow, Qt.GlobalColor.lightGray) + + nRow += 1 + del data + + ## metadata.db + def LoadCalibreDB(self, path:str ) -> None: + pathDB = os.path.join(path, "metadata.db") + if False == os.path.exists(pathDB): + util.DbgOut(f"Calibre DB not found: {pathDB}", True) + return + + DB = MgrCalibreDB.MgrCalibreDB(pathDB) + listItems = DB.GetBookListforUI_ArcList() + for item in listItems: + strID = item[4] + strTitle = item[0] + strAuthor = item[1] + strHasCover = item[2] + strBookPath = item[3] + + nRow = self.tableWidget_DB.rowCount() + self.tableWidget_DB.setRowCount(nRow + 1) + + self.tableWidget_DB.setItem(nRow, 0, QTableWidgetItem(strTitle)) + self.tableWidget_DB.setItem(nRow, 1, QTableWidgetItem(strAuthor)) + + itemCover = QTableWidgetItem(strHasCover) + if "0" == strHasCover: + itemCover.setForeground(QColor(255, 0, 0)) + + self.tableWidget_DB.setItem(nRow, 2, itemCover) + + # format, uncompressed_size, name + tupleData = DB.GetDataByBookID(int(strID)) + itemExt = QTableWidgetItem(tupleData[0]) + pathArc = os.path.join(path, strBookPath, f"{tupleData[2]}.{tupleData[0]}") + if False == os.path.exists(pathArc): + itemExt.setForeground(QColor(255, 0, 0)) + + self.tableWidget_DB.setItem(nRow, 3, itemExt) + self.tableWidget_DB.setItem(nRow, 4, QTableWidgetItem(strID)) + + # + def on_btnFolderAdd_clicked(self): + folder_path = QFileDialog.getExistingDirectory(self, '폴더 선택', '') + + if True == util.IsEmptyStr(folder_path): + return + + pathAbs = os.path.abspath(folder_path) + if self.listWidget_Folders.findItems(pathAbs, Qt.MatchFlag.MatchExactly): + util.DbgOut(f"폴더가 이미 추가되어 있습니다: {pathAbs}", True) + return + + self.listWidget_Folders.addItem(pathAbs) + + # + def on_btnFolderDel_clicked(self): + selected_items = self.listWidget_Folders.selectedItems() + for item in selected_items: + row = self.listWidget_Folders.row(item) + self.listWidget_Folders.takeItem(row) + + # + def on_btnFolderParse_clicked(self): + itemCount = self.listWidget_Folders.count() + if 0 == itemCount: + util.DbgOut("폴더가 선택되지 않았습니다.", True) + return + + # 중복 검사을 위한 딕셔너리 초기화 + self.m_DictDuplicate.clear() + # 다운로드가 완료되지 않은 만화 목록 초기화 + self.m_ListNotCompleteMngas.clear() + # 테이블 위젯 비우기 + self.tableWidget_Src.setRowCount(0) + + for idx in range(itemCount): + item = self.listWidget_Folders.item(idx) + if item is None: + continue + + self.LoadSrcFolder(item.text()) + + # + def on_btnDB_clicked(self): + folder_path = QFileDialog.getExistingDirectory(self, '폴더 선택', '') + + if True == util.IsEmptyStr(folder_path): + return + + if self.edit_DB.text() == folder_path: + return + + self.edit_DB.setText(folder_path) + self.LoadCalibreDB(folder_path) + + # + def on_tableWidget_Src_headerClicked(self, logicalIndex: int): + # 정렬 상태 확인 및 변경 + HorHeader = self.tableWidget_Src.horizontalHeader() + if HorHeader is None: + return + + if HorHeader.sortIndicatorOrder() == Qt.SortOrder.AscendingOrder: + sort_order = Qt.SortOrder.DescendingOrder + else: + sort_order = Qt.SortOrder.AscendingOrder + + HorHeader.setSortIndicator(logicalIndex, sort_order) + self.tableWidget_Src.sortItems(logicalIndex, HorHeader.sortIndicatorOrder() ) + + # + def on_tableWidget_Src_itemSelectionChanged(self): + pass + + # + def on_tableWidget_DB_itemSelectionChanged(self): + pass + + # + def on_btn_Emptyfolder_clicked(self): + folder_path = QFileDialog.getExistingDirectory(self, '폴더 선택', '') + + for pathItem in self.m_ListNotCompleteMngas: + # 유효하지 않은 경로면 건드리지 말자 + if False == os.path.exists(pathItem): + continue + + # 폴더 이동 + try: + baseName = os.path.basename(pathItem) + pathDest = os.path.join(folder_path, baseName) + shutil.move(pathItem, pathDest) + + if True == os.path.exists(pathDest): + util.DbgOut(f"폴더 이동 완료: {pathItem} -> {pathDest}") + + except Exception as e: + strMsg = f"폴더 이동 중 오류 발생: {e}" + util.DbgOut(strMsg, True) + QMessageBox.critical(self, "Error", strMsg) + + # + def on_btn_ChkDuplicate_clicked(self): + if len(self.m_DictDuplicate) == 0: + util.DbgOut("중복 검사할 데이터가 없습니다.", True) + return + + for nID, paths in self.m_DictDuplicate.items(): + if len(paths) <= 1: + continue + + # 첫번째 만화의 해시를 검사한다. + # -해시가 멀쩡하면 정상으로 판단, 2번째부터는 그냥 지운다. + # 만약 해시가 이상하면 해시가 멀쩡한 놈을 찾는다. + # -멀쩡한 놈을 찾으면 그걸 보존, 다른 나머지를 지운다. + # 해시가 전부 이상하면 일단 전부 딴데로 백업. + # - 해당하는 파일을 다운받거나, 중복되는 것 중에서 조합해서 복원. 그건 다른 데서 하자. + + + # + def on_btn_Archive_clicked(self): + pass + + # + def on_btn_EnterCalibre_clicked(self): + pass + +""" +if __name__ == '__main__': + app = QApplication(sys.argv) + + app.setQuitOnLastWindowClosed(True) + main_window = MyApp() + #main_window.show() + + sys.exit(app.exec_()) +""" + diff --git a/MgrPupilColDB.py b/MgrPupilColDB.py new file mode 100644 index 0000000..6a4f71f --- /dev/null +++ b/MgrPupilColDB.py @@ -0,0 +1,53 @@ +import sqlite3 + +import UtilPack as util +import MgrSQLiteDB as MyDB + +class MgrPupilColDB(MyDB.MgrSQLiteDB): + + # 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 AddBook(self, strHitomiID: str, strArchiveID: str, strTitle: str, listAuthors: list[str], + listGroups: list[str], listSeries: list[str], strType: str, strLanguage: str, + listTags: list[str], strDescription: str) -> int: + pass + + def AddAuthor(self, strName: str) -> int: + pass + + def AddTag(self, strName: str) -> int: + pass + + def AddsSeries(self, strName: str) -> int: + pass + + def AddGroup(self, strName: str) -> int: + pass + + def AddArchive(self, strName: str, setArcPath: str) -> int: + pass + + def AddFileInfo(self) -> int: + pass + + + + + diff --git a/MgrSQLiteDB.py b/MgrSQLiteDB.py new file mode 100644 index 0000000..ae8c1f3 --- /dev/null +++ b/MgrSQLiteDB.py @@ -0,0 +1,96 @@ +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() + + def init(self): + # 데이터베이스 연결 (파일이 없으면 새로 생성됨) + self.conn = sqlite3.connect(self.path) + self.cursor = self.conn.cursor() + + # 쿼리를 실행한다. 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] + \ No newline at end of file diff --git a/layoutImg.jpeg b/Res/layoutImg.jpeg similarity index 100% rename from layoutImg.jpeg rename to Res/layoutImg.jpeg diff --git a/UI.py b/UI.py deleted file mode 100644 index da8fe4b..0000000 --- a/UI.py +++ /dev/null @@ -1,323 +0,0 @@ -import sys -import os -import UtilPack as util -import MgrCalibreDB as calDB -import MgrCalibreLibs as calLib -import pupildata as pupil -import GetArc_Hitomi as hitomi - -from io import BytesIO - -from PyQt5.QtCore import Qt, QUrl, QSettings, QSize, QPoint, QByteArray, QRect -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): - super().resizeEvent(event) - rcDesktop = QApplication.primaryScreen().availableGeometry() - rcWnd = self.geometry() - - if False == rcDesktop.contains(rcWnd): - self.setGeometry(rcDesktop) - - pixmap = self.label_Image.pixmap() - if None != pixmap: - pixmapScaled = pixmap.scaled(self.label_Image.size()) - self.label_Image.setPixmap(pixmapScaled) - - self.update() - - # - 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() - - 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_Right(self): - self.list_Items = QListWidget(self) - self.list_Items.setFixedWidth(300) - self.list_Items.itemSelectionChanged.connect(self.on_Item_SelChanged_Items) - - self.list_BookDetail = QListWidget(self) - - layout = QVBoxLayout() - layout.addWidget(self.list_Items, stretch = 2) - layout.addWidget(self.list_BookDetail, stretch = 1) - - return layout - - - def MakeUI(self): - layout_L = self.MakeUI_Left() - - # show Image in middle Layout - self.label_Image = QLabel(self) - - layout_R = self.MakeUI_Right() - - # 레이아웃 설정 - layout = QHBoxLayout() - layout.addLayout(layout_L, stretch = 1) - layout.addWidget(self.label_Image, stretch= 4) - layout.addLayout(layout_R, stretch = 1) - - return layout - -# - 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) - self.AddSourceFoldertoList(folder_path) - - - def AddSourceFoldertoList(self, folder_path): - #캘리버 서재 DB 파일을 먼저 확인 - pathCalDB = os.path.join( folder_path, "metadata.db") - if True == os.path.exists(pathCalDB): - self.AddSourceCalibreDB(pathCalDB) - else: - self.AddSourceFolderContents(folder_path) - - - def AddSourceCalibreDB(self, pathCalDB): - db = calDB.MgrCalibreDB(pathCalDB) - - if None == db: - return - - db.init() - listBooks = db.GetBookListforUI_ArcList() - for book in listBooks: - # 이름은 폴더/DB 파일이름 - FolderName = util.GetParentDirName(pathCalDB, 0) - item = QListWidgetItem(f"{FolderName}/{book[0]}") - - bookpath = os.path.join(pathCalDB, book[1]) - listfiles = util.FindFileFromExt(bookpath, "cbz") - if 0 < len(listfiles): - bookpath = os.path.join(bookpath, listfiles[0]) - - item.setData(Qt.UserRole, bookpath) - self.list_ArcList.addItem(item) - - return - - def AddSourceFolderContents(self, 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 [".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): - list_id = [] - for idx in range(self.list_ArcList.count()): - item = self.list_ArcList.item(idx) - - # 일단 파일 이름에 포함된 Hitomi ID 를 추출해 낸다. - results = util.GetTextInBrakets(item.text()) - if 0 >= len(results): - continue - - if False == results[0].isdigit() or 0 >= int(results[0]): - continue - - list_id.append(results[0]) - - hitomi.GetListSearchResult(list_id) - - - 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 None == pathTarget or 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) - - for item in listContents: - if ".metadata" in item: - pBytes = util.GetZippedFileByte(pathTarget, item) - print(pBytes.decode("utf-8")) - pupildata = pupil.PupuilInfoFile(pBytes.decode("utf-8")) - print(pupildata.GetInfo()) - - 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) - - # - 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() ) - - -if __name__ == '__main__': - app = QApplication(sys.argv) - - app.setQuitOnLastWindowClosed(True) - main_window = MyApp() - #main_window.show() - - sys.exit(app.exec_()) - diff --git a/UtilPack.py b/UtilPack.py index 733e3ff..1dfa78b 100644 --- a/UtilPack.py +++ b/UtilPack.py @@ -7,19 +7,22 @@ import zipfile import shutil import difflib import subprocess +import hashlib from pathlib import Path m_dbgLevel = 0 -listDbgStr = [] +listDbgStr: list[str] = [] + # -def IsEmptyStr(string): +def IsEmptyStr(string: str) -> bool: temp = f"{string}" return 0 == len(temp.strip()) + # -def GetCurrentTime(): +def GetCurrentTime() -> str: # 현재 시간을 구하고 구조체로 변환 current_time_struct = time.localtime() @@ -35,21 +38,24 @@ def GetCurrentTime(): return strRet + #for debug -def DbgOut(strInput, bPrint = False): +def DbgOut(strInput:str, bPrint:bool = False): strMsg = (f"{GetCurrentTime()} : {strInput}") listDbgStr.append(strMsg) if True == bPrint: print(strMsg) + # def printDbgMessages(): for line in listDbgStr: print(line) + # -def SaveDbgMessages(Path): +def SaveDbgMessages(Path: str): try: with open(Path, 'w') as file: for line in listDbgStr: @@ -57,10 +63,11 @@ def SaveDbgMessages(Path): except IOError: DbgOut(f"Error: Could not write to the file at {Path}.", True) + # 입력된 경로의 자식 폴더를 찾아 반환한다. # 반환하는 리스트는 리커시브 - 손자, 증손자 폴더까지 전부 포함한다 -def ListSubDirectories(root_dir): - subdirectories = [] +def ListSubDirectories(root_dir:str)-> list[str]: + subdirectories: list[str] = [] # root_dir에서 하위 디렉토리 및 파일 목록을 얻음 for dirpath, dirnames, filenames in os.walk(root_dir): @@ -74,32 +81,36 @@ def ListSubDirectories(root_dir): return subdirectories # 자식 폴더를 구해온다. 직계 자식만 -def ListChildDirectories(pathDir): - listRet = [] - for name in os.listdir(pathDir): +def ListChildDirectories(pathDir:str, bVisibleOnly: bool = True) -> list[str]: + listRet:list[str] = [] + listTemp = os.listdir(pathDir) + for name in listTemp: pathChild = os.path.join(pathDir, name) if os.path.isdir(pathChild): - listRet.append(pathChild) - + if True == bVisibleOnly and name.startswith('.'): + continue + + listRet.append(name) + return listRet -# 파일목록만 구해온다. 자식 폴더에 있는건 무시. -def ListContainFiles(pathDir): - listRet = [] - for name in os.listdir(pathDir): +# PathDir 에 지정된 폴더의 파일목록만 구해온다. 자식 폴더에 있는건 무시. +def ListContainFiles(pathDir:str, bVisibleOnly:bool = True)-> list[str]: + listRet:list[str] = [] + listTemp = os.listdir(pathDir) + for name in listTemp: pathChild = os.path.join(pathDir, name) - if not os.path.isdir(pathChild): - listRet.append(pathChild) - + if os.path.isfile(pathChild): + if True == bVisibleOnly and name.startswith('.'): + continue + + listRet.append(name) + return listRet -# -def ListFileExtRcr(pathTrg, listExt): - listRet= [] - - if False == isinstance(listExt, list): - print("ext must list") - return +# 리스트에 담긴 확장자와 같은 확장자를 가진 파일을 찾아서 리스트로 반환 +def ListFileExtRcr(pathTrg: str, listExt: list[str]) -> list[str]: + listRet:list[str] = [] # pathTrg의 하위 디렉토리 및 파일 목록을 얻음 for dirpath, dirnames, filenames in os.walk(pathTrg): @@ -111,7 +122,7 @@ def ListFileExtRcr(pathTrg, listExt): return listRet # 입력된 경로에서 부모 폴더를 찾는다. 0 은 자기자신 1은 부모, 숫자대로 위로. -def GetParentDirName(FullPath, nUp): +def GetParentDirName(FullPath : str, nUp : int)->str: parts = FullPath.split(os.sep) nTrgIdx = 0 @@ -126,24 +137,24 @@ def GetParentDirName(FullPath, nUp): # 입력된 경로가 자식 폴더를 가지고 있는지 판단한다.- 최종 폴더인지 여부 # 자식이 없으면 True, 자식이 있으면 False -def IsFinalFolder(path): +def IsFinalFolder(path : str) -> bool: bRet = True contents = os.listdir(path) for item in contents: if True == os.path.isdir(item): - bRt = False + bRet = False break return bRet; # 어떤 경로 안에서 특정 확장자의 파일을 뽑아내어 그 리스트를 반환한다. -def FindFileFromExt(path, ext): +def FindFileFromExt(path: str, ext: str)-> list[str]: bDot = False if 0 <= ext.find('.'): bDot = True - listRet = [] + listRet:list[str] = [] if False == os.path.exists(path): return listRet @@ -159,7 +170,7 @@ def FindFileFromExt(path, ext): return listRet # 파일 이름에서 확장자를 뽑아낸다. True : '.' 을 포함한다. -def GetExtStr(file_path, bDot = True): +def GetExtStr(file_path: str, bDot: bool = True)-> str: retStr = "" # 파일 경로에서 마지막 점을 찾아 확장자를 추출 last_dot_index = file_path.rfind('.') @@ -174,7 +185,7 @@ def GetExtStr(file_path, bDot = True): return retStr # 문자열에 포함된 단어를 지운다. -def RmvSubString(mainString, subString): +def RmvSubString(mainString: str, subString: str)-> str: # 문자열에서 부분 문자열의 인덱스를 찾습니다. strIdx = mainString.find(subString) if strIdx == -1: # 부분 문자열이 존재하지 않으면 그대로 반환합니다. @@ -186,12 +197,12 @@ def RmvSubString(mainString, subString): return mainString[:strIdx] + mainString[endIdx:] # -def ExtractZIP(zip_file, extract_to): +def ExtractZIP(zip_file: str, extract_to: str): with zipfile.ZipFile(zip_file, 'r') as zf: zf.extractall(extract_to) # -def CreateZIP(output_zip, *files): +def CreateZIP(output_zip: str, files: list[str]) -> bool: with zipfile.ZipFile(output_zip, 'w') as zf: for file in files: pathTemp = os.path.join('root', os.path.basename(file)) @@ -204,7 +215,7 @@ def CreateZIP(output_zip, *files): return bRet # 파일 리스트에 들어있는 파일만 골라서 압축을 합니다. 상대경로를 제거하는게 기본값. -def CreateZIPShell(zipName, *files, bRmvRPath = True): +def CreateZIPShell(zipName: str, files: list[str], bRmvRPath: bool = True) -> bool: command = "zip " if True == bRmvRPath: @@ -214,16 +225,7 @@ def CreateZIPShell(zipName, *files, bRmvRPath = True): # 이중 리스트인 이유를 모르겠다. for file in files: - strTemp = "" - if isinstance(file, list): - strTemp = ' '.join(file) - else: - strTemp = f"\"{file}\" " - - command += strTemp - - # for item in file: - # command += f"\"{item}\" " + command += f"\"{file}\" " result = subprocess.run(command, shell=True, capture_output=True, text=True) @@ -234,7 +236,7 @@ def CreateZIPShell(zipName, *files, bRmvRPath = True): return bRet # 특정 확장자만 쉘을 이용해서 압축한다 -def CreateZIPShExt(zipName, TrgExt): +def CreateZIPShExt(zipName: str, TrgExt: str)-> bool: command = f"zip -j {zipName} *.{TrgExt}" result = subprocess.run(command, shell=True, capture_output=True, text=True) @@ -246,8 +248,8 @@ def CreateZIPShExt(zipName, TrgExt): return bRet # 압축 파일 내의 모든 파일 및 디렉토리 목록 가져오기 -def GetZipContentList(path): - if None == path or not os.path.isfile(path): +def GetZipContentList(path: str) -> list[str]: + if True == IsEmptyStr(path) or not os.path.isfile(path): return [] listRet = [] @@ -257,22 +259,18 @@ def GetZipContentList(path): return listRet # -def GetZippedFileByte(pathZip, FileName): - if None == pathZip or not os.path.isfile(pathZip): - return None - - retBytes = None - with zipfile.ZipFile( pathZip , 'r') as zip_file: - if not FileName in zip_file.namelist(): - return None - - with zip_file.open(FileName) as file: - retBytes = file.read() +def GetZippedFileByte(pathZip: str, FileName: str) -> bytes: + retBytes:bytes = bytes() + if True == os.path.isfile(pathZip): + with zipfile.ZipFile( pathZip , 'r') as zip_file: + # 압축 파일 내의 특정 파일을 읽기 + with zip_file.open(FileName) as file: + retBytes = file.read() return retBytes # JSON 을 트리 구조로 출력한다. -def PrintJSONTree(data, indent=0): +def PrintJSONTree(data, indent: int=0 ) -> None: if isinstance(data, dict): for key, value in data.items(): print(' ' * indent + str(key)) @@ -295,10 +293,54 @@ def UUIDGenRandom(): return random_uuid # 이름을 기반으로 UUID 생성 -def UUIDGenName(SeedName): +def UUIDGenName(SeedName:str): namespace_uuid = uuid.uuid5(uuid.NAMESPACE_DNS, SeedName) return namespace_uuid # -def GetTextInBrakets(text): - return re.findall(r'\[(.*?)\]', text) \ No newline at end of file +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("파일이 손상되었거나 다릅니다!") +""" \ No newline at end of file diff --git a/StoreXLS.py b/Util_XLS.py similarity index 100% rename from StoreXLS.py rename to Util_XLS.py diff --git a/main.py b/main.py index 6708d87..ab8d492 100644 --- a/main.py +++ b/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 sys +import MgrCalibreUI + +from PyQt5.QtWidgets import QApplication + + def main(argc, argv): - if argc != 2: - printUsage() - return - - trgPath = argv[1] - #getHitomi.GetSearchResult("2890685") - #etEhentai.GetSearchResult("artist%3A%22kotomi+yo-ji%24%22") + app = QApplication(argv) - #mgrCal.Start() - #util.printDbgMessages() + app.setQuitOnLastWindowClosed(True) + main_window = MgrCalibreUI.MyApp() + main_window.show() - #artist:"kotomi yo-ji$" - #"artist%3A%22kotomi+yo-ji%24%22" - - #tempxls = xls.DBXLStorage("./temp.xls") - #tempxls.DBXLSOpen() - #tempxls.AddArtistInfo("Kuno Inu", "/artist/kunoinu/") - #tempxls.AddSeriesInfo("Original", "/serires/original/") - #tempxls.AddTagInfo("female:bondage", "/tag/bondage/") - #tempxls.AddTagInfo("female:slave", "/tag/slave/") - #tempxls.DBXLSClose() - - - if False == os.path.exists(trgPath): - printUsage() - return - - listPaths = util.ListSubDirectories(trgPath) - util.DbgOut(f"Folder Cnt : {len(listPaths)}") - - PupilDB_EXT = ".data" - PupilImg_EXT = ".img" - CalbCvr = "cover" - CalbCvr_EXT = ".jpg" - - nIdx = 0 - # 각 pupil 만화책 폴더 -> path - for path in listPaths: - - # 퍼필에서 저장한 만화책 폴더의 절대경로를 구한다 - # 퍼필 만화책 정보 파일을 파싱 - # - 만약 만화책 파일이 하나도 없거나 정보 파일과 다르다면.. - # - - 정보 파일만 적당히 백업하고 다음으로 넘어간다 - # - # 캘리버 정보 파일로 저장한다 - # 퍼필에서 생성한 표지 파일을 찾는다. - # webp 인지 확인해서 jpg 로 변환하여 저장, JPG 라면 이름만 바꾼다 - # 만약 없다면 1번 파일을 적당히 줄여서 표지 파일을 생성 - - # 이미지 파일 리스트를 생성 - # 압축한다 - - # 압축파일, 캘리버 정보 파일, 표지 파일이 다 있는지 확인하고 메시지 출력 - # 하나라도 없으면 에러 리스트에 저장 - - nIdx += 1 - - -def printUsage(): - print("Usage : python main.py ") + sys.exit(app.exec_()) + # For Main Loop if __name__ == '__main__': diff --git a/rarzipcbz.py b/rarzipcbz.py deleted file mode 100644 index 6c5734e..0000000 --- a/rarzipcbz.py +++ /dev/null @@ -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) diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 31a3dcf..0000000 --- a/requirements.txt +++ /dev/null @@ -1,9 +0,0 @@ -beautifulsoup4==4.12.3 -bs4==0.0.2 -PyQt5==5.15.11 -PyQt5-Qt5==5.15.2 -PyQt5_sip==12.15.0 -rarfile==4.2 -setuptools==75.6.0 -soupsieve==2.6 -wheel==0.45.1 diff --git a/temp.py b/temp.py deleted file mode 100644 index b7c95d0..0000000 --- a/temp.py +++ /dev/null @@ -1,55 +0,0 @@ -import sys -from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QVBoxLayout, QLineEdit, QHBoxLayout - - -class MyApp(QWidget): - - def __init__(self): - super().__init__() - self.initUI() - - def initUI(self): - self.edit = QLineEdit("", self) - btnOK = QPushButton("OK", self) - btnOK.clicked.connect(self.on_click_btn_OK) - layout1 = QHBoxLayout() - layout1.addWidget(self.edit) - layout1.addWidget(btnOK) - - # 버튼 생성 - btn1 = QPushButton('Button 1', self) - btn2 = QPushButton('Button 2', self) - - # 버튼 클릭 시 발생하는 이벤트 연결 - btn1.clicked.connect(self.on_click_btn1) - btn2.clicked.connect(self.on_click_btn2) - - # 레이아웃 설정 - layout = QVBoxLayout() - layout.addLayout(layout1) - layout.addWidget(btn1) - layout.addWidget(btn2) - - # 레이아웃을 윈도우에 적용 - self.setLayout(layout) - - self.setWindowTitle('My First Application') - self.move(300, 300) - self.resize(400, 200) - self.show() - - # 버튼 1 클릭 시 호출되는 메서드 - def on_click_btn1(self): - print('Button 1 clicked!') - - # 버튼 2 클릭 시 호출되는 메서드 - def on_click_btn2(self): - print('Button 2 clicked!') - - def on_click_btn_OK(self): - print(self.edit.text()) - -if __name__ == '__main__': - app = QApplication(sys.argv) - ex = MyApp() - sys.exit(app.exec_()) \ No newline at end of file diff --git a/ui_Tki.py b/ui_Tki.py deleted file mode 100644 index 3d054d1..0000000 --- a/ui_Tki.py +++ /dev/null @@ -1,26 +0,0 @@ -import os -import sys -import tkinter as tk - -def sayhello(): - print("Hello") - -def main(argc, argv): - # 기본 창 설정 - root = tk.Tk() - root.title("Hello World App") # 창 제목 설정 - root.geometry("300x200") # 창 크기 설정 - - # 버튼 생성 - button = tk.Button(root, text="Click Me", command=sayhello) - - # 버튼을 창에 배치 - button.pack(pady=20) - - # 이벤트 루프 실행 - root.mainloop() - -if __name__ == '__main__': - argc = len(sys.argv) - argv = sys.argv - main(argc, argv) \ No newline at end of file