BookMakerAdder/main.py

567 lines
23 KiB
Python

import sys
import os
import sqlite3
import subprocess
import json
from datetime import datetime, time
import logging
import traceback
import pandas as pd
from PySide6.QtWidgets import (
QApplication, QMainWindow, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QLineEdit,
QComboBox, QSpinBox, QCheckBox, QProgressBar, QTextEdit, QFileDialog, QWidget, QMessageBox, QDialog, QTableWidget, QTableWidgetItem, QAbstractItemView, QDialogButtonBox
)
from PySide6.QtCore import Qt, QThread, Signal, QSettings
class BookmarkWorker(QThread):
progress = Signal(int)
log = Signal(str)
completed = Signal() # 작업 완료 시 호출
def __init__(self, bookmarks, folder_name, bookmarks_path, chrome_path, remove_existing):
super().__init__()
self.bookmarks = bookmarks
self.folder_name = folder_name
self.bookmarks_path = bookmarks_path
self.chrome_path = chrome_path
self.remove_existing = remove_existing
def run(self):
try:
# JSON 파일 읽기
if not os.path.exists(self.bookmarks_path):
self.log.emit(f"즐겨찾기 JSON 파일을 찾을 수 없습니다: {self.bookmarks_path}")
return
try:
with open(self.bookmarks_path, "r", encoding="utf-8") as file:
file_content = file.read().strip()
if not file_content: # 파일이 비어 있는 경우
bookmarks_data = {"roots": {"bookmark_bar": {"children": []}}}
self.log.emit("JSON 파일이 비어 있어 기본값으로 초기화합니다.")
else:
bookmarks_data = json.loads(file_content)
except json.JSONDecodeError as e:
self.log.emit(f"JSON 파일 파싱 중 오류 발생: {str(e)}")
bookmarks_data = {"roots": {"bookmark_bar": {"children": []}}}
self.log.emit("JSON 파일을 기본값으로 초기화합니다.")
# 기존 북마크 제거
if self.remove_existing:
bookmarks_data["roots"]["bookmark_bar"] = self.remove_existing_bookmarks(
bookmarks_data["roots"]["bookmark_bar"]
)
# 북마크 추가
total_bookmarks = len(self.bookmarks)
bookmark_bar = bookmarks_data["roots"]["bookmark_bar"]
# 상위 폴더 이름 생성
current_time = datetime.now().strftime("%m-%d-%H-%M-%S") # 현재 날짜 및 시간 포맷
parent_folder_name = f"거상북마크-{current_time}" # 상위 폴더 이름
# 상위 폴더 생성
parent_folder = {
"type": "folder",
"name": parent_folder_name,
"children": []
}
# 하위 폴더 생성
chunk_size = 100 # 하위 폴더에 넣을 북마크 수
for idx, chunk_start in enumerate(range(0, total_bookmarks, chunk_size), start=1):
# 하위 폴더 이름 생성
folder_name = f"거상북마크-{self.folder_name}-{idx}" # 하위 폴더 이름 (예: 거상북마크-중국-1)
sub_folder = {
"type": "folder",
"name": folder_name,
"children": []
}
# 하위 폴더에 북마크 추가
for bookmark in self.bookmarks[chunk_start:chunk_start + chunk_size]:
sub_folder["children"].append({
"type": "url",
"name": bookmark['name'], # 몰 이름을 북마크 이름으로 설정
"url": bookmark['url']
})
# 상위 폴더에 하위 폴더 추가
parent_folder["children"].append(sub_folder)
# 진행률 업데이트
progress = int((chunk_start + len(self.bookmarks[chunk_start:chunk_start + chunk_size])) / total_bookmarks * 100)
self.progress.emit(progress)
# 즐겨찾기 바에 상위 폴더 추가
bookmark_bar["children"].append(parent_folder)
# 수정된 JSON 파일 저장
with open(self.bookmarks_path, "w", encoding="utf-8") as file:
json.dump(bookmarks_data, file, indent=4, ensure_ascii=False)
self.log.emit("즐겨찾기 추가 작업이 완료되었습니다!")
# 작업 완료 시 크롬 실행
if os.path.exists(self.chrome_path):
subprocess.Popen([self.chrome_path, "chrome://bookmarks/"])
self.log.emit("크롬 북마크 관리 페이지를 열었습니다.")
else:
self.log.emit(f"크롬 실행 파일을 찾을 수 없습니다: {self.chrome_path}")
self.completed.emit()
except Exception as e:
self.log.emit(f"오류 발생: {str(e)}", exc_info=True)
self.progress.emit(0)
def remove_existing_bookmarks(self, node):
"""
북마크 데이터를 재귀적으로 탐색하여 '거상북마크'로 시작하는 모든 폴더를 제거합니다.
:param node: 북마크 데이터의 현재 노드
:return: 필터링된 북마크 데이터
"""
if not isinstance(node, dict):
return node
# 폴더 이름이 '거상북마크'로 시작하면 제거
if node.get("type") == "folder" and node.get("name", "").startswith("거상북마크"):
self.log.emit(f"제거된 폴더: {node.get('name')}")
return None
# 자식(children)이 있는 경우 재귀적으로 탐색
if "children" in node:
node["children"] = [
self.remove_existing_bookmarks(child)
for child in node["children"]
if self.remove_existing_bookmarks(child) is not None
]
return node
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
logging.basicConfig(level=logging.DEBUG, format="%(asctime)s - %(levelname)s - %(message)s")
self.logger = logging.getLogger(__name__)
# datetime을 문자열로 변환하는 어댑터 등록
sqlite3.register_adapter(datetime, lambda dt: dt.strftime("%Y-%m-%d %H:%M:%S"))
# 문자열을 datetime 객체로 변환하는 컨버터 등록
sqlite3.register_converter("DATETIME", lambda s: datetime.strptime(s.decode("utf-8"), "%Y-%m-%d %H:%M:%S"))
# 고정된 비밀번호
self.stored_password = "365"
# QSettings 초기화 (비밀번호 저장 및 불러오기용)
self.settings = QSettings("WhenRideMycar", "BookmarkAdder")
# 비밀번호 확인 창 표시
self.password = None
self.show_password_dialog()
# 프로그램 실행 가능 여부 확인
if self.password != self.stored_password:
QMessageBox.critical(self, "비밀번호 오류", "비밀번호가 일치하지 않습니다. 프로그램을 종료합니다.")
sys.exit()
self.setWindowTitle("크롬 즐겨찾기 추가 프로그램 (by 내차는언제타냐 feat.거상110+님)")
self.setGeometry(300, 300, 800, 500)
# UI 구성
self.layout = QVBoxLayout()
self.buttons_layout = QHBoxLayout()
self.filter_layout = QHBoxLayout()
# DB 입력 버튼
self.db_input_button = QPushButton("DB 입력")
self.db_input_button.setToolTip("엑셀 파일을 선택하여 DB에 저장합니다. 기존 DB를 제거하거나 추가로 데이터를 입력할 수 있습니다.")
self.db_input_button.clicked.connect(self.load_excel)
# 기존 DB 제거 체크박스
self.remove_db_checkbox = QCheckBox("기존 DB 제거")
self.remove_db_checkbox.setToolTip("체크하면 기존 DB를 삭제하고 새롭게 데이터를 입력합니다.")
# 데이터 보기 버튼
self.view_data_button = QPushButton("데이터 보기")
self.view_data_button.setToolTip("DB 내용을 테이블 형식으로 표시하고 데이터를 수정할 수 있습니다.")
self.view_data_button.setEnabled(False)
self.view_data_button.clicked.connect(self.view_data)
# 드롭다운: 국가
self.country_label = QLabel("국가")
self.country_dropdown = QComboBox()
self.country_dropdown.addItems(["미국", "유럽", "중국", "일본", "한국", "기타", "랜덤"])
self.country_dropdown.setCurrentText("중국")
# 드롭다운: 등급
self.grade_label = QLabel("등급")
self.grade_dropdown = QComboBox()
self.grade_dropdown.addItems(["일반", "파워", "빅파워", "랜덤"])
self.grade_dropdown.setCurrentText("랜덤")
# 스핀박스: 갯수
self.count_label = QLabel("갯수")
self.count_spinbox = QSpinBox()
self.count_spinbox.setMinimum(100)
self.count_spinbox.setMaximum(1000)
self.count_spinbox.setSingleStep(100)
self.count_spinbox.setValue(100)
# 기존 북마크 제거 체크박스
self.remove_existing_checkbox = QCheckBox("기존 북마크 제거")
self.remove_existing_checkbox.setToolTip("체크하면 '거상북마크'로 시작하는 모든 북마크를 제거합니다.")
# 실행 버튼
self.run_button = QPushButton("실행")
self.run_button.setToolTip("선택된 필터 조건에 따라 DB에서 데이터를 가져와 북마크를 추가합니다.")
self.run_button.clicked.connect(self.run_task)
# 로그 박스
self.log_box = QTextEdit()
self.log_box.setReadOnly(True)
# 프로그레스 바
self.progress_bar = QProgressBar()
# 필터 레이아웃 구성
self.filter_layout.addWidget(self.country_label)
self.filter_layout.addWidget(self.country_dropdown)
self.filter_layout.addWidget(QLabel(" "))
self.filter_layout.addWidget(self.grade_label)
self.filter_layout.addWidget(self.grade_dropdown)
self.filter_layout.addWidget(QLabel(" "))
self.filter_layout.addWidget(self.count_label)
self.filter_layout.addWidget(self.count_spinbox)
self.filter_layout.addWidget(QLabel(" "))
self.filter_layout.addWidget(self.remove_existing_checkbox)
# 버튼 레이아웃 구성
self.buttons_layout.addWidget(self.db_input_button)
self.buttons_layout.addWidget(self.remove_db_checkbox)
self.buttons_layout.addWidget(self.view_data_button)
self.buttons_layout.addWidget(self.run_button)
# 메인 레이아웃 구성
self.layout.addLayout(self.filter_layout)
self.layout.addLayout(self.buttons_layout)
self.layout.addWidget(self.log_box)
self.layout.addWidget(self.progress_bar)
# 메인 위젯 설정
self.main_widget = QWidget()
self.main_widget.setLayout(self.layout)
self.setCentralWidget(self.main_widget)
# 상태 변수
self.db_path = "markets.db"
# DB 파일 확인
self.check_db()
def show_password_dialog(self):
"""비밀번호 입력 창을 표시"""
dialog = QDialog(self)
dialog.setWindowTitle("비밀번호 입력")
dialog.setFixedSize(300, 150)
layout = QVBoxLayout(dialog)
# 비밀번호 입력 필드
password_label = QLabel("비밀번호:")
password_input = QLineEdit()
password_input.setEchoMode(QLineEdit.Password)
# 저장된 비밀번호 불러오기
saved_password = self.settings.value("password", "")
if saved_password:
password_input.setText(saved_password)
# 비밀번호 저장 체크박스
save_password_checkbox = QCheckBox("비밀번호 저장")
save_password_checkbox.setChecked(bool(saved_password)) # 저장된 비밀번호가 있으면 체크
# 확인 버튼
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel, parent=dialog)
button_box.accepted.connect(dialog.accept)
button_box.rejected.connect(dialog.reject)
# 레이아웃 구성
layout.addWidget(password_label)
layout.addWidget(password_input)
layout.addWidget(save_password_checkbox)
layout.addWidget(button_box)
# 다이얼로그 실행
if dialog.exec() == QDialog.Accepted:
self.password = password_input.text()
# 비밀번호 저장 처리
if save_password_checkbox.isChecked():
self.settings.setValue("password", self.password)
else:
self.settings.remove("password")
else:
self.password = None
def log(self, message, exc_info=False):
"""
메시지를 로그에 출력하고, GUI 로그 박스에도 표시
:param message: 출력할 메시지
:param exc_info: True일 경우 traceback 정보 포함
"""
if exc_info:
# traceback 정보 포함하여 로깅
self.logger.error(message, exc_info=True)
error_traceback = traceback.format_exc()
self.log_box.append(f"{message}\n{error_traceback}")
else:
self.logger.info(message)
self.log_box.append(message)
def check_db(self):
if not os.path.exists(self.db_path):
QMessageBox.warning(self, "DB 없음", "DB 파일이 없습니다. 엑셀을 지정하여 DB를 생성해주세요.")
else:
self.log("DB 파일이 로드되었습니다.")
self.view_data_button.setEnabled(True)
def load_excel(self):
file_path, _ = QFileDialog.getOpenFileName(self, "엑셀 파일 선택", "", "Excel Files (*.xlsx *.xls)")
if not file_path:
return
try:
self.log("엑셀 파일을 불러오는 중...")
# 파일 확장자 확인
ext = os.path.splitext(file_path)[-1].lower()
# 필수 열 이름 정의
required_columns = ['country', 'mall_grade', 'mall_name', 'mall_url']
# 엑셀 파일 읽기
if ext == ".xls":
try:
import xlrd
df = pd.read_excel(file_path, sheet_name=0, engine="xlrd")
except ImportError:
self.log("xlrd 라이브러리가 필요합니다. 'pip install xlrd>=2.0.1' 명령으로 설치하세요.")
return
except xlrd.biffh.XLRDError as e:
self.log(f"엑셀 파일을 열 수 없습니다. 파일 형식을 확인하세요: {e}")
return
elif ext == ".xlsx":
try:
df = pd.read_excel(file_path, sheet_name=0, engine="openpyxl")
except Exception as e:
self.log(f"엑셀 파일을 열 수 없습니다: {e}", exc_info=True)
return
else:
self.log("지원되지 않는 파일 형식입니다. .xls 또는 .xlsx 파일을 선택하세요.")
return
# 엑셀에서 가져온 열과 필수 열 비교
available_columns = [col for col in required_columns if col in df.columns]
if not available_columns:
self.log("엑셀 파일에 필수 열이 없습니다. 필요한 열: " + ", ".join(required_columns))
return
# 필요한 열만 가져오기
df = df[available_columns]
# 누락된 열은 빈 값으로 추가
for col in required_columns:
if col not in df.columns:
df[col] = "" # 누락된 열은 빈 값으로 채움
# 열 이름을 정렬하여 설정
df = df[required_columns]
# datetime.time 타입 데이터를 문자열로 변환
def convert_time_to_string(x):
try:
if isinstance(x, time): # 시간이면 변환
return x.strftime("%H:%M:%S")
return x # 아니면 그대로 반환
except Exception as e:
self.log(f"데이터 변환 중 에러 발생: {str(e)}")
return x
if 'mall_name' in df.columns:
df['mall_name'] = df['mall_name'].apply(convert_time_to_string)
if 'mall_url' in df.columns:
df['mall_url'] = df['mall_url'].apply(convert_time_to_string)
conn = sqlite3.connect(self.db_path)
if self.remove_db_checkbox.isChecked():
# 기존 DB 제거 및 새 테이블 생성
conn.execute("DROP TABLE IF EXISTS markets")
conn.execute("""
CREATE TABLE markets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
country TEXT,
mall_grade TEXT,
mall_name TEXT,
mall_url TEXT
)
""")
self.log("기존 DB가 제거되었습니다. 새롭게 생성되었습니다.")
# 데이터 저장, 중복 방지
df.drop_duplicates(subset=['country', 'mall_grade', 'mall_name', 'mall_url'], inplace=True)
# id 열을 자동 생성하지 않으므로, 기존 테이블 스키마를 유지하면서 데이터를 삽입
for _, row in df.iterrows():
conn.execute("""
INSERT INTO markets (country, mall_grade, mall_name, mall_url)
VALUES (?, ?, ?, ?)
""", (row['country'], row['mall_grade'], row['mall_name'], row['mall_url']))
conn.commit()
conn.close()
self.log("DB 저장 완료")
except Exception as e:
self.log(f"엑셀 파일을 불러오는 중 에러 발생: {str(e)}", exc_info=True)
def view_data(self):
try:
conn = sqlite3.connect(self.db_path)
query = "SELECT * FROM markets"
df = pd.read_sql_query(query, conn)
conn.close()
dialog = QDialog(self)
dialog.setWindowTitle("DB 데이터 보기")
dialog.setGeometry(100, 100, 800, 400)
table = QTableWidget(dialog)
table.setRowCount(len(df))
table.setColumnCount(len(df.columns))
table.setHorizontalHeaderLabels(df.columns)
for i, row in df.iterrows():
for j, value in enumerate(row):
item = QTableWidgetItem(str(value))
if j == 0: # id 열 (편집 불가능)
item.setFlags(item.flags() & ~Qt.ItemIsEditable)
table.setItem(i, j, item)
# 네 번째 열 너비 조정
table.setColumnWidth(4, table.columnWidth(3) * 2)
# 테이블 정렬 활성화
table.setSortingEnabled(True)
# 정렬을 위한 헤더 클릭 이벤트 연결
table.horizontalHeader().sectionClicked.connect(lambda index: self.sort_table(table, index))
table.cellChanged.connect(lambda: self.confirm_edit(table))
table.resize(780, 380)
dialog.exec()
except Exception as e:
QMessageBox.critical(self, "오류", f"DB 데이터를 불러오는 중 오류 발생: {e}", exc_info=True)
def sort_table(self, table, column_index):
"""
테이블 데이터를 정렬하는 함수
:param table: QTableWidget
:param column_index: 정렬할 열 인덱스
"""
try:
order = table.horizontalHeader().sortIndicatorOrder() # 현재 정렬 방향 확인
table.sortItems(column_index, order) # 해당 열에 따라 정렬
self.log(f"{column_index + 1}번째 열을 정렬했습니다. 방향: {'오름차순' if order == Qt.AscendingOrder else '내림차순'}")
except Exception as e:
self.log(f"테이블 정렬 중 오류 발생: {str(e)}", exc_info=True)
def confirm_edit(self, table):
reply = QMessageBox.question(self, "확인", "수정된 데이터를 저장하시겠습니까?", QMessageBox.Yes | QMessageBox.No)
if reply == QMessageBox.Yes:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
for row in range(table.rowCount()):
# id는 수정하지 않으므로 첫 번째 열은 그대로 사용
record_id = table.item(row, 0).text()
country = table.item(row, 1).text()
mall_grade = table.item(row, 2).text()
mall_name = table.item(row, 3).text()
mall_url = table.item(row, 4).text()
# id를 기준으로 업데이트
cursor.execute("""
UPDATE markets
SET country = ?, mall_grade = ?, mall_name = ?, mall_url = ?
WHERE id = ?
""", (country, mall_grade, mall_name, mall_url, record_id))
conn.commit()
conn.close()
self.log("수정된 데이터가 저장되었습니다.")
else:
self.log("수정이 취소되었습니다.")
def run_task(self):
country = self.country_dropdown.currentText()
grade = self.grade_dropdown.currentText()
count = self.count_spinbox.value()
remove_existing = self.remove_existing_checkbox.isChecked()
conn = sqlite3.connect(self.db_path)
query = "SELECT mall_name AS name, mall_url AS url FROM markets WHERE 1=1"
# 국가 필터
if country != "랜덤":
query += f" AND country = '{country}'"
# 등급 필터
if grade != "랜덤":
query += f" AND mall_grade = '{grade}'"
# 랜덤 정렬 추가
query += " ORDER BY RANDOM()"
# 갯수 제한 추가
query += f" LIMIT {count}"
try:
df = pd.read_sql_query(query, conn)
conn.close()
self.bookmarks = df.to_dict("records")
self.log(f"{len(self.bookmarks)}개의 북마크를 추출했습니다.")
except Exception as e:
self.log(f"DB 쿼리 실행 중 오류 발생: {str(e)}", exc_info=True)
conn.close()
return
folder_name = f"거상북마크-{grade}"
self.bookmarks_path = os.path.expanduser(r"~\AppData\Local\Google\Chrome\User Data\Default\Bookmarks")
self.chrome_path = r"C:\Program Files\Google\Chrome\Application\chrome.exe"
self.worker = BookmarkWorker(self.bookmarks, folder_name, self.bookmarks_path, self.chrome_path, remove_existing)
self.worker.progress.connect(self.progress_bar.setValue)
self.worker.log.connect(self.log)
self.worker.completed.connect(self.task_completed)
self.worker.start()
def task_completed(self):
self.log("작업이 완료되었습니다!")
QMessageBox.information(self, "완료", "즐겨찾기 추가 작업이 완료되었습니다.")
def main():
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()