Skip to content

Commit ebd33e1

Browse files
committed
feat: 全面重构GUI与日志系统
本次提交对项目进行了大规模的现代化改造,核心在于GUI的视觉革新和日志系统的完全重构,旨在提升用户体验和代码质量。 **主要改动:** ### 1. GUI 全面革新 - **新主题**: 废弃 `qdarkstyle`,引入名为 "Otaku-Sync Soft & Fresh Theme" 的自定义浅色主题,视觉上更柔和、现代化。 - **映射编辑器重构**: 彻底改造了映射文件编辑器,增加了搜索、左右分栏布局、未保存状态提示 (`*`) 及安全退出确认,显著提升了易用性。 - **国际化**: 为Qt标准对话框添加了中文翻译。 ### 2. 日志系统重构 - **标准化**: 全面迁移到 Python 官方的 `logging` 模块,并引入 `rich` 库美化终端输出。 - **分级显示**: - **CLI**: 默认日志更简洁。通过设置 `LOG_LEVEL=DEBUG` 环境变量可开启“调试模式”,显示含文件路径和行号的详细信息。 - **GUI**: 日志在界面上保持干净,只显示核心信息。 - **代码解耦**: 移除了旧的 `patch_logger` 猴子补丁机制,代码更健壮。 ### 3. Bug 修复 - 修正了 `scripts/inspect_notion_fields.py` 脚本中因在同步函数里使用 `await` 导致的语法错误。 ### 4. 文档更新 - 同步更新了 `GEMINI.md`, `utils/GEMINI.md`, `gui/GEMINI.md` 等文档,以准确反映新的架构和功能。
1 parent 3c57548 commit ebd33e1

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+670
-663
lines changed

GEMINI.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@ Otaku-Sync 是一款自动化工具,其核心目标是从多个在线数据源
1313
- **`core/`**: 包含了所有的核心业务逻辑,如数据抓取、信息处理和同步。此模块与具体的 UI 实现无关。
1414
- **`core/interaction.py`**: 定义了一个抽象接口,所有需要用户输入或交互的场景(例如,让用户从列表中选择、确认操作或输入文本)都必须通过此接口进行。
1515
- **UI 实现**:
16-
- **GUI (`run_gui.py`, `gui/`, `utils/gui_bridge.py`)**: 通过一个基于 Qt (PySide6) 的现代化图形界面实现交互提供者接口。该界面采用了 `qdarkstyle` 库提供的暗色主题,并通过 `gui/style.qss` 进行自定义美化。主窗口使用标签页 (`QTabWidget`) 来组织不同的功能模块(如批处理工具、映射文件编辑器),布局则通过自定义的 `FlowLayout` 等组件实现,提供了用户友好的操作体验。
16+
- **GUI (`run_gui.py`, `gui/`)**: 通过一个基于 Qt (PySide6) 的现代化图形界面实现交互提供者接口。该界面采用在 `gui/style.qss` 中定义的自定义浅色主题,取代了原有的 `qdarkstyle` 库,提供了更统一和现代化的视觉体验。主窗口使用标签页 (`QTabWidget`) 来组织不同的功能模块(如批处理工具、映射文件编辑器),布局则通过自定义的 `FlowLayout` 等组件实现,提供了用户友好的操作体验。
1717
- **CLI (`main.py`)**: 通过简单的命令行提示(如 `input()`)来实现交互提供者接口。
1818

1919
这种架构保证了项目的高度可维护性和可扩展性。例如,未来可以在不改动任何核心逻辑的情况下,轻松地为项目添加一个新的 Web 前端。
2020

2121
## 3. 主要入口点
2222

23-
- **`run_gui.py`**: **GUI 模式**的入口文件。它负责初始化 `QApplication`应用 `qdarkstyle` 和自定义样式,并启动主窗口 `MainWindow`。这是为大多数用户推荐的运行方式。
23+
- **`run_gui.py`**: **GUI 模式**的入口文件。它负责初始化 `QApplication`加载并应用在 `gui/style.qss` 中定义的自定义样式,并启动主窗口 `MainWindow`。这是为大多数用户推荐的运行方式。
2424
- **`main.py`**: **CLI 模式**的入口文件。它包含一个主异步循环,负责提示用户输入、处理单个游戏,然后重复此过程。
2525

2626
## 4. 关键模块与目录

batch_updater.py

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
# batch_updater.py
22
import asyncio
3+
import logging
34
import re
45
from tqdm import tqdm
56
from typing import List, Dict, Any
67

78
from core.init import init_context, close_context
8-
from utils import logger
99
from config.config_token import GAME_DB_ID, BRAND_DB_ID, CHARACTER_DB_ID
1010
from config.config_fields import FIELDS
1111

@@ -126,7 +126,7 @@ async def preprocess_item(context, page: Dict[str, Any], db_key: str) -> Dict[st
126126
}
127127
except Exception as e:
128128
page_title = context["notion"].get_page_title(page)
129-
logger.warn(f"预处理 '{page_title}' 时失败: {e}")
129+
logging.warning(f"⚠️ 预处理 '{page_title}' 时失败: {e}")
130130
return None
131131

132132

@@ -171,7 +171,7 @@ async def write_item_to_notion(context, item_data: Dict[str, Any], db_key: str):
171171
warned_keys = set()
172172
await context["bangumi"].create_or_update_character(bangumi_data, warned_keys)
173173
except Exception as e:
174-
logger.error(f"写入页面 '{page_title}' ({page_id}) 时出错: {e}")
174+
logging.error(f"写入页面 '{page_title}' ({page_id}) 时出错: {e}")
175175

176176

177177
async def batch_update(context, dbs_to_update: List[str]):
@@ -180,14 +180,14 @@ async def batch_update(context, dbs_to_update: List[str]):
180180

181181
for db_key in dbs_to_update:
182182
config = DB_CONFIG[db_key]
183-
logger.step(f"开始处理 {config['name']}...")
183+
logging.info(f"🚀 开始处理 {config['name']}...")
184184

185185
all_pages = await notion_client.get_all_pages_from_db(config["id"])
186186
if not all_pages:
187-
logger.warn(f"{config['name']} 中没有找到任何页面。")
187+
logging.warning(f"⚠️ {config['name']} 中没有找到任何页面。")
188188
continue
189189

190-
logger.info(f"共找到 {len(all_pages)} 个条目,将以每批 {CONCURRENCY_LIMIT} 个并发处理...")
190+
logging.info(f"共找到 {len(all_pages)} 个条目,将以每批 {CONCURRENCY_LIMIT} 个并发处理...")
191191

192192
# 使用tqdm包装分块器,以批次为单位显示进度
193193
for page_chunk in tqdm(
@@ -215,13 +215,13 @@ async def batch_update(context, dbs_to_update: List[str]):
215215
async with interaction_lock:
216216
await write_item_to_notion(context, item, db_key)
217217

218-
logger.success(f"{config['name']} 处理完成!")
218+
logging.info(f"{config['name']} 处理完成!")
219219

220220

221221
async def main():
222222
dbs_to_update = get_user_choice()
223223
if not dbs_to_update:
224-
logger.info("用户选择退出。")
224+
logging.info("🔍 用户选择退出。")
225225
return
226226

227227
context = await init_context()
@@ -243,26 +243,26 @@ async def new_handle_new_key_wrapper(
243243
target_db_id,
244244
)
245245
if result and result not in schema_manager.get_schema(target_db_id):
246-
logger.system(f"检测到新属性 '{result}' 已创建,正在刷新数据库结构...")
246+
logging.info(f"🔧 检测到新属性 '{result}' 已创建,正在刷新数据库结构...")
247247
db_key = DB_ID_TO_KEY_MAP.get(target_db_id)
248248
if db_key:
249249
await schema_manager.initialize_schema(target_db_id, DB_CONFIG[db_key]["name"])
250-
logger.success("数据库结构缓存已刷新!")
250+
logging.info("✅ 数据库结构缓存已刷新!")
251251
return result
252252

253253
BangumiMappingManager.handle_new_key = new_handle_new_key_wrapper
254254

255255
try:
256256
await batch_update(context, dbs_to_update)
257257
except (KeyboardInterrupt, asyncio.CancelledError):
258-
logger.warn("\n接收到中断信号,正在退出...")
258+
logging.warning("\n⚠️ 接收到中断信号,正在退出...")
259259
except Exception as e:
260-
logger.error(f"批量更新流程出现未捕获的严重错误: {e}")
260+
logging.error(f"批量更新流程出现未捕获的严重错误: {e}")
261261
finally:
262-
logger.system("正在清理资源...")
262+
logging.info("🔧 正在清理资源...")
263263
await close_context(context)
264264
context["brand_cache"].save_cache(context["brand_extra_info_cache"])
265-
logger.system("批量更新程序已安全退出。")
265+
logging.info("✅ 批量更新程序已安全退出。")
266266

267267

268268
# get_user_choice() 和 __main__ 部分保持不变
@@ -291,4 +291,6 @@ def get_user_choice():
291291

292292

293293
if __name__ == "__main__":
294-
asyncio.run(main())
294+
from utils.logger import setup_logging_for_cli
295+
setup_logging_for_cli()
296+
asyncio.run(main())

clients/bangumi_client.py

Lines changed: 31 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# clients/bangumi_client.py
22
# 该模块用于与 Bangumi API 交互,获取游戏和角色信息
33
import asyncio
4+
import logging
45
from rapidfuzz import fuzz
56

67
import json
@@ -18,7 +19,6 @@
1819
from core.interaction import InteractionProvider
1920
from core.mapping_manager import BangumiMappingManager
2021
from core.schema_manager import NotionSchemaManager
21-
from utils import logger
2222

2323
API_TOKEN = BANGUMI_TOKEN
2424
HEADERS_API = {
@@ -83,11 +83,11 @@ async def _search(self, keyword: str):
8383
try:
8484
resp = await self.client.post(url, headers=self.headers, json=payload, timeout=15)
8585
if resp.status_code != 200:
86-
logger.warn(f"[Bangumi] API搜索失败: {resp.status_code}")
86+
logging.warning(f"⚠️ [Bangumi] API搜索失败: {resp.status_code}")
8787
return []
8888
return resp.json().get("data", [])
8989
except httpx.RequestError as e:
90-
logger.error(f"[Bangumi] API请求异常: {e}")
90+
logging.error(f"[Bangumi] API请求异常: {e}")
9191
return []
9292

9393
async def search_and_select_bangumi_id(self, keyword: str) -> str | None:
@@ -119,23 +119,23 @@ async def search_and_select_bangumi_id(self, keyword: str) -> str | None:
119119
if clean_title(item.get("name", "")) and (
120120
clean_title(keyword) in clean_title(item.get("name", ""))
121121
):
122-
logger.info(f"[Bangumi] 子串匹配成功: {item['name']},视为同一作品")
122+
logging.info(f"🔍 [Bangumi] 子串匹配成功: {item['name']},视为同一作品")
123123
return str(item["id"])
124124
if candidates and candidates[0][0] >= self.similarity_threshold:
125125
best = candidates[0][1]
126-
logger.info(f"[Bangumi] 自动匹配成功: {best['name']} (相似度 {candidates[0][0]:.2f})")
126+
logging.info(f"🔍 [Bangumi] 自动匹配成功: {best['name']} (相似度 {candidates[0][0]:.2f})")
127127
return str(best["id"])
128128
if candidates and candidates[0][0] >= 0.7:
129129
best = candidates[0][1]
130130
if clean_title(best["name"]) in clean_title(keyword) or clean_title(
131131
keyword
132132
) in clean_title(best["name"]):
133-
logger.info(
134-
f"[Bangumi] 模糊匹配成功(放宽判定): {best['name']} (相似度 {candidates[0][0]:.2f})"
133+
logging.info(
134+
f"🔍 [Bangumi] 模糊匹配成功(放宽判定): {best['name']} (相似度 {candidates[0][0]:.2f})"
135135
)
136136
return str(best["id"])
137137

138-
logger.warn("Bangumi自动匹配相似度不足,请手动选择:")
138+
logging.warning("⚠️ Bangumi自动匹配相似度不足,请手动选择:")
139139

140140
# Format candidates for display in GUI
141141
gui_candidates = []
@@ -335,7 +335,7 @@ async def create_or_update_character(self, char: dict, warned_keys: Set[str]) ->
335335
prop_type = self.schema.get_property_type(CHARACTER_DB_ID, notion_prop_name)
336336
if not prop_type:
337337
if notion_prop_name not in warned_keys:
338-
logger.warn(f"角色属性 '{notion_prop_name}' 在 Notion 角色库中不存在,已跳过。")
338+
logging.warning(f"⚠️ 角色属性 '{notion_prop_name}' 在 Notion 角色库中不存在,已跳过。")
339339
warned_keys.add(notion_prop_name)
340340
continue
341341
if prop_type == "title":
@@ -361,19 +361,19 @@ async def create_or_update_character(self, char: dict, warned_keys: Set[str]) ->
361361
"PATCH", f"https://api.notion.com/v1/pages/{existing_id}", {"properties": props}
362362
)
363363
if resp:
364-
logger.info(f"角色已存在,已更新:{char['name']}")
364+
logging.info(f"🔍 角色已存在,已更新:{char['name']}")
365365
return existing_id if resp else None
366366
else:
367367
payload = {"parent": {"database_id": CHARACTER_DB_ID}, "properties": props}
368368
resp = await self.notion._request("POST", "https://api.notion.com/v1/pages", payload)
369369
if resp:
370-
logger.success(f"新角色已创建:{char['name']}")
370+
logging.info(f"新角色已创建:{char['name']}")
371371
return resp.get("id") if resp else None
372372

373373
async def create_or_link_characters(self, game_page_id: str, subject_id: str):
374374
characters = await self.fetch_characters(subject_id)
375375
if not characters:
376-
logger.info("未找到任何 Bangumi 角色信息,跳过角色关联。")
376+
logging.info("🔍 未找到任何 Bangumi 角色信息,跳过角色关联。")
377377
patch = {
378378
"properties": {
379379
FIELDS["bangumi_url"]: {"url": f"https://bangumi.tv/subject/{subject_id}"}
@@ -391,7 +391,7 @@ async def create_or_link_characters(self, game_page_id: str, subject_id: str):
391391
character_relations = [{"id": cid} for cid in char_ids if cid]
392392
page_data = await self.notion.get_page(game_page_id)
393393
if not page_data:
394-
logger.error(f"无法获取游戏页面 {game_page_id} 的当前状态,跳过声优补充。")
394+
logging.error(f"无法获取游戏页面 {game_page_id} 的当前状态,跳过声优补充。")
395395
return
396396
patch_props = {
397397
FIELDS["bangumi_url"]: {"url": f"https://bangumi.tv/subject/{subject_id}"},
@@ -401,32 +401,32 @@ async def create_or_link_characters(self, game_page_id: str, subject_id: str):
401401
page_data.get("properties", {}).get(FIELDS["voice_actor"], {}).get("multi_select", [])
402402
)
403403
if not existing_vcs:
404-
logger.info("游戏页面声优信息为空,尝试从 Bangumi 角色数据中补充...")
404+
logging.info("🔍 游戏页面声优信息为空,尝试从 Bangumi 角色数据中补充...")
405405
all_cvs = {ch["声优"].strip() for ch in characters if ch.get("声优")}
406406
if all_cvs:
407-
logger.success(f"已为【游戏页面】补充 {len(all_cvs)} 位声优。")
407+
logging.info(f"已为【游戏页面】补充 {len(all_cvs)} 位声优。")
408408
patch_props[FIELDS["voice_actor"]] = {
409409
"multi_select": [{"name": name} for name in sorted(all_cvs)]
410410
}
411411
else:
412-
logger.info("Bangumi 角色数据中也未找到声优信息以供补充。")
412+
logging.info("🔍 Bangumi 角色数据中也未找到声优信息以供补充。")
413413
else:
414-
logger.info("游戏页面已存在声优信息,跳过补充。")
414+
logging.info("🔍 游戏页面已存在声优信息,跳过补充。")
415415
await self.notion._request(
416416
"PATCH", f"https://api.notion.com/v1/pages/{game_page_id}", {"properties": patch_props}
417417
)
418-
logger.success("Bangumi 角色信息同步与关联完成。")
418+
logging.info("✅ Bangumi 角色信息同步与关联完成。")
419419

420420
async def fetch_brand_info_from_bangumi(self, brand_name: str) -> dict | None:
421421
"""[已重构] 搜索品牌,找到ID后调用 fetch_person_by_id 获取完整信息。"""
422422

423423
async def search_brand(keyword: str):
424-
logger.info(f"[Bangumi] 正在搜索品牌关键词: {keyword}")
424+
logging.info(f"🔍 [Bangumi] 正在搜索品牌关键词: {keyword}")
425425
url = "https://api.bgm.tv/v0/search/persons"
426426
data = {"keyword": keyword, "filter": {"career": ["artist", "director", "producer"]}}
427427
resp = await self.client.post(url, headers=self.headers, json=data)
428428
if resp.status_code != 200:
429-
logger.error(f"[Bangumi] 品牌搜索失败,状态码: {resp.status_code}")
429+
logging.error(f"[Bangumi] 品牌搜索失败,状态码: {resp.status_code}")
430430
return []
431431
return resp.json().get("data", [])
432432

@@ -458,28 +458,28 @@ async def search_brand(keyword: str):
458458
best_score, best_match = candidates[0] if candidates else (0, None)
459459

460460
if not best_match or best_score < 0.7:
461-
logger.warn(f"未找到相似度高于阈值的品牌(最高: {best_score:.2f})")
461+
logging.warning(f"⚠️ 未找到相似度高于阈值的品牌(最高: {best_score:.2f})")
462462
return None
463463

464464
person_id = best_match.get("id")
465465
if not person_id:
466-
logger.warn("最佳匹配项缺少ID,无法获取详细信息。")
466+
logging.warning("⚠️ 最佳匹配项缺少ID,无法获取详细信息。")
467467
return None
468468

469-
logger.success(
470-
f"[Bangumi] 搜索匹配成功: {best_match.get('name')} (ID: {person_id}, 相似度: {best_score:.2f})"
469+
logging.info(
470+
f"[Bangumi] 搜索匹配成功: {best_match.get('name')} (ID: {person_id}, 相似度: {best_score:.2f})"
471471
)
472472
return await self.fetch_person_by_id(str(person_id))
473473

474474
async def fetch_person_by_id(self, person_id: str) -> dict | None:
475475
"""[已重构] 通过 Person ID 直接获取并处理厂商/个人信息,作为唯一的数据处理源。"""
476476
url = f"https://api.bgm.tv/v0/persons/{person_id}"
477-
logger.info(f"[Bangumi] 正在通过 ID 直接获取品牌信息: {person_id}")
477+
logging.info(f"🔍 [Bangumi] 正在通过 ID 直接获取品牌信息: {person_id}")
478478
try:
479479
resp = await self.client.get(url, headers=self.headers)
480480
if resp.status_code != 200:
481-
logger.error(
482-
f"[Bangumi] 品牌信息获取失败,ID: {person_id}, 状态码: {resp.status_code}"
481+
logging.error(
482+
f"[Bangumi] 品牌信息获取失败,ID: {person_id}, 状态码: {resp.status_code}"
483483
)
484484
return None
485485

@@ -499,11 +499,11 @@ async def fetch_person_by_id(self, person_id: str) -> dict | None:
499499
}
500500
brand_info.update(infobox_data)
501501

502-
logger.success(f"[Bangumi] 已成功获取并处理品牌: {person_data.get('name')}")
502+
logging.info(f"[Bangumi] 已成功获取并处理品牌: {person_data.get('name')}")
503503
return brand_info
504504

505505
except Exception as e:
506-
logger.error(f"[Bangumi] 通过ID获取品牌信息时发生异常: {e}")
506+
logging.error(f"[Bangumi] 通过ID获取品牌信息时发生异常: {e}")
507507
return None
508508

509509
async def fetch_and_prepare_character_data(self, character_id: str) -> dict | None:
@@ -512,7 +512,7 @@ async def fetch_and_prepare_character_data(self, character_id: str) -> dict | No
512512
char_detail_url = f"https://api.bgm.tv/v0/characters/{character_id}"
513513
resp = await self.client.get(char_detail_url, headers=self.headers)
514514
if resp.status_code != 200:
515-
logger.error(f"获取角色 {character_id} 详情失败: 状态码 {resp.status_code}")
515+
logging.error(f"获取角色 {character_id} 详情失败: 状态码 {resp.status_code}")
516516
return None
517517

518518
detail = resp.json()
@@ -536,5 +536,5 @@ async def fetch_and_prepare_character_data(self, character_id: str) -> dict | No
536536

537537
return char_data_to_update
538538
except Exception as e:
539-
logger.error(f"处理角色 {character_id} 数据时出错: {e}")
539+
logging.error(f"处理角色 {character_id} 数据时出错: {e}")
540540
return None

0 commit comments

Comments
 (0)