diff --git a/douyu/config.py b/douyu/config.py index 4c5b61e..2f8d379 100644 --- a/douyu/config.py +++ b/douyu/config.py @@ -5,6 +5,8 @@ from pathlib import Path from dataclasses import dataclass from typing import List, Optional +from .email_verifier import get_email_config_for_account + @dataclass class Account: @@ -45,13 +47,14 @@ class Config: """获取账号列表""" accounts = [] for acc in self._config.get('accounts', []): + email_config = get_email_config_for_account(acc['email']) accounts.append(Account( username=acc['username'], password=acc['password'], email=acc['email'], email_password=acc['email_password'], - email_imap_server=acc.get('email_imap_server', ''), - email_imap_port=acc.get('email_imap_port', 993), + email_imap_server=acc.get('email_imap_server') or email_config['server'], + email_imap_port=acc.get('email_imap_port') or email_config['port'], )) return accounts diff --git a/douyu/email_verifier.py b/douyu/email_verifier.py index 8db645b..d7ae3a6 100644 --- a/douyu/email_verifier.py +++ b/douyu/email_verifier.py @@ -40,6 +40,7 @@ class EmailVerifier: """连接IMAP服务器""" try: logger.info(f"连接IMAP服务器: {self.imap_server}:{self.imap_port}") + self._validate_login_text() self._connection = imaplib.IMAP4_SSL( self.imap_server, self.imap_port, @@ -51,6 +52,17 @@ class EmailVerifier: logger.error(f"IMAP连接失败: {e}") raise + def _validate_login_text(self) -> None: + """提前检查IMAP登录字段,给出比ascii编码异常更明确的提示。""" + for label, value in (("邮箱账号", self.username), ("邮箱密码/授权码", self.password)): + try: + value.encode("ascii") + except UnicodeEncodeError as exc: + raise ValueError( + f"{label}包含中文或其他非ASCII字符,IMAP无法登录;" + "请检查导入格式是否为:用户名|密码|邮箱|邮箱密码" + ) from exc + def disconnect(self) -> None: """断开IMAP连接""" if self._connection: @@ -324,6 +336,7 @@ class EmailVerifier: def get_email_config_for_account(email_address: str) -> dict: """根据邮箱地址自动配置IMAP服务器""" configs = { + 'bdhg.xyz': {'server': 'mail.bdhg.xyz', 'port': 993}, 'qq.com': {'server': 'imap.qq.com', 'port': 993}, '163.com': {'server': 'imap.163.com', 'port': 993}, '126.com': {'server': 'imap.126.com', 'port': 993}, diff --git a/gui/app.py b/gui/app.py index f8629d7..f9eb4cf 100644 --- a/gui/app.py +++ b/gui/app.py @@ -9,11 +9,26 @@ from pathlib import Path from queue import Queue, Empty from typing import List +from loguru import logger + from douyu.config import Account, ProxyConfig +from utils import setup_logger from .login_worker import LoginWorker, parse_accounts_text from .widgets import AccountTable, LogPanel +LOG_LEVELS = ('DEBUG', 'INFO', 'WARNING', 'ERROR') +LOG_LEVEL_TAGS = { + 'TRACE': 'debug', + 'DEBUG': 'debug', + 'INFO': 'info', + 'SUCCESS': 'success', + 'WARNING': 'warning', + 'ERROR': 'error', + 'CRITICAL': 'error', +} + + class DouyuLoginApp: """斗鱼批量登录GUI应用""" @@ -31,6 +46,8 @@ class DouyuLoginApp: # 加载配置 self.proxy_config = self._load_proxy_config() self.geetest_retries = self._load_geetest_retries() + self.log_level = self._load_log_level() + self._configure_logging(self.log_level) # 创建主窗口 self.root = tk.Tk() @@ -77,6 +94,45 @@ class DouyuLoginApp: except: return 5 + def _load_log_level(self) -> str: + """从配置文件加载默认日志等级。""" + try: + import yaml + with open(self.config_path, 'r', encoding='utf-8') as f: + config = yaml.safe_load(f) + level = config.get('log', {}).get('level', 'INFO').upper() + return level if level in LOG_LEVELS else 'INFO' + except: + return 'INFO' + + def _configure_logging(self, level: str) -> None: + """配置终端和GUI日志输出等级。""" + self.log_level = level if level in LOG_LEVELS else 'INFO' + setup_logger(level=self.log_level) + logger.add( + self._enqueue_log_record, + level=self.log_level, + format='{message}', + colorize=False, + ) + + def _enqueue_log_record(self, message) -> None: + """把loguru日志转发到GUI日志队列。""" + record = message.record + level = record['level'].name + tag = LOG_LEVEL_TAGS.get(level, 'info') + text = ( + f"{record['name']}:{record['function']}:{record['line']} | " + f"{record['message']}" + ) + self.log_queue.put((tag, text)) + + def _on_log_level_change(self, _event=None) -> None: + """切换日志等级。""" + level = self.log_level_var.get().upper() + self._configure_logging(level) + self.log_panel.append_log('info', f'日志等级已切换为: {level}') + def _create_ui(self): """创建用户界面""" # 主容器 @@ -163,9 +219,13 @@ class DouyuLoginApp: self.proxy_https_entry = ttk.Entry(static_frame, textvariable=self.proxy_https_var, width=20) self.proxy_https_entry.pack(side='left', padx=(5, 0)) + # 右侧配置:极验和日志 + side_frame = ttk.Frame(config_frame) + side_frame.pack(side='right', fill='y', padx=(5, 0)) + # 极验配置 - geetest_frame = ttk.LabelFrame(config_frame, text='极验配置', padding=10) - geetest_frame.pack(side='right', fill='y', padx=(5, 0)) + geetest_frame = ttk.LabelFrame(side_frame, text='极验配置', padding=10) + geetest_frame.pack(fill='x') ttk.Label(geetest_frame, text='最大重试:').pack(anchor='w') self.geetest_retries_var = tk.IntVar(value=self.geetest_retries) @@ -173,6 +233,22 @@ class DouyuLoginApp: textvariable=self.geetest_retries_var) retries_spin.pack(anchor='w', pady=(5, 0)) + # 日志配置 + log_frame = ttk.LabelFrame(side_frame, text='日志配置', padding=10) + log_frame.pack(fill='x', pady=(8, 0)) + + ttk.Label(log_frame, text='等级:').pack(anchor='w') + self.log_level_var = tk.StringVar(value=self.log_level) + log_level_combo = ttk.Combobox( + log_frame, + values=LOG_LEVELS, + textvariable=self.log_level_var, + state='readonly', + width=10, + ) + log_level_combo.pack(anchor='w', pady=(5, 0)) + log_level_combo.bind('<>', self._on_log_level_change) + # 初始状态 self._on_proxy_toggle() @@ -333,6 +409,8 @@ class DouyuLoginApp: # 获取当前GUI配置 proxy_config = self._get_proxy_config() geetest_retries = self.geetest_retries_var.get() + log_level = self.log_level_var.get().upper() + self._configure_logging(log_level) # 验证代理配置 if proxy_config.enabled: @@ -366,6 +444,7 @@ class DouyuLoginApp: self.log_panel.append_log('info', '未使用代理') self.log_panel.append_log('info', f'极验最大重试: {geetest_retries} 次') + self.log_panel.append_log('info', f'日志等级: {log_level}') self.progress_var.set('登录中...') # 启动登录线程 diff --git a/gui/login_worker.py b/gui/login_worker.py index c0be273..79ba3cb 100644 --- a/gui/login_worker.py +++ b/gui/login_worker.py @@ -1,5 +1,6 @@ """登录工作线程模块""" +import re import threading import time from queue import Queue @@ -8,33 +9,31 @@ from loguru import logger from douyu import DouyuLogin from douyu.config import Account, ProxyConfig +from douyu.email_verifier import get_email_config_for_account -# IMAP服务器映射 -IMAP_SERVERS = { - 'qq.com': 'imap.qq.com', - '163.com': 'imap.163.com', - '126.com': 'imap.126.com', - 'gmail.com': 'imap.gmail.com', - 'outlook.com': 'outlook.office365.com', - 'hotmail.com': 'outlook.office365.com', - 'yeah.net': 'imap.yeah.net', - 'sohu.com': 'imap.sohu.com', - 'sina.com': 'imap.sina.com', -} +EMAIL_PATTERN = re.compile(r'^[^\s@|]+@[^\s@|]+\.[^\s@|]+$') def get_imap_server(email: str) -> str: """从邮箱地址推导IMAP服务器""" - domain = email.split('@')[-1].lower() - return IMAP_SERVERS.get(domain, f'imap.{domain}') + return get_email_config_for_account(email)['server'] + + +def _is_ascii(value: str) -> bool: + """检查字符串是否为ASCII。""" + try: + value.encode('ascii') + return True + except UnicodeEncodeError: + return False def parse_accounts_text(text: str) -> list[Account]: """ 解析账号文本 - 格式:用户名|密码|邮箱|邮箱密码(每行一个) + 格式:用户名|密码|邮箱|邮箱密码 Returns: Account列表 @@ -47,7 +46,7 @@ def parse_accounts_text(text: str) -> list[Account]: continue parts = line.split('|') - if len(parts) < 4: + if len(parts) != 4: logger.warning(f"第{line_num}行格式错误,需要4个字段,实际{len(parts)}个: {line}") continue @@ -57,6 +56,17 @@ def parse_accounts_text(text: str) -> list[Account]: logger.warning(f"第{line_num}行存在空字段: {line}") continue + if not EMAIL_PATTERN.match(email) or not _is_ascii(email): + logger.warning( + f"第{line_num}行邮箱格式不正确: {email}," + "请确认格式为:用户名|密码|邮箱|邮箱密码" + ) + continue + + if not _is_ascii(email_password): + logger.warning(f"第{line_num}行邮箱密码/授权码包含非ASCII字符,IMAP可能无法登录") + continue + accounts.append(Account( username=username, password=password,