"""账号导入工具。""" from __future__ import annotations import csv import io import json from pathlib import Path from typing import Any, Iterable, Mapping import yaml from loguru import logger from douyu.config import Account from douyu.email_verifier import get_email_config_for_account from .login_worker import EMAIL_PATTERN, _is_ascii, parse_accounts_text FIELD_ALIASES = { "username": {"username", "user", "account", "账号", "用户名", "斗鱼账号"}, "password": {"password", "pass", "pwd", "密码", "登录密码"}, "email": {"email", "mail", "邮箱", "邮箱地址"}, "email_password": { "email_password", "email_pass", "email_pwd", "mail_password", "mail_pass", "邮箱密码", "邮箱授权码", "授权码", }, "email_imap_server": {"email_imap_server", "imap_server", "imap", "imap服务器"}, "email_imap_port": {"email_imap_port", "imap_port", "imap端口"}, } NORMALIZED_ALIASES = { "".join(alias.lower().replace("-", "_").split()): field for field, aliases in FIELD_ALIASES.items() for alias in aliases } def load_accounts_from_file(filepath: str | Path) -> list[Account]: """从文件导入账号,支持txt/csv/json/yaml。""" path = Path(filepath) text = _read_text(path) suffix = path.suffix.lower() if suffix == ".json": return _load_structured_accounts(text, path.name, "json") if suffix in {".yaml", ".yml"}: return _load_structured_accounts(text, path.name, "yaml") if suffix == ".csv": accounts = _parse_csv_text(text, path.name) return accounts or parse_accounts_text(text) return parse_accounts_text(text) def _read_text(path: Path) -> str: """按常见编码读取文本文件。""" last_error: Exception | None = None for encoding in ("utf-8-sig", "utf-8", "gb18030"): try: return path.read_text(encoding=encoding) except UnicodeDecodeError as exc: last_error = exc if last_error: raise last_error return path.read_text(encoding="utf-8") def _load_structured_accounts(text: str, source: str, file_type: str) -> list[Account]: """读取JSON/YAML中的账号列表。""" try: if file_type == "json": data = json.loads(text) else: data = yaml.safe_load(text) except Exception as exc: logger.warning(f"{source} 结构化解析失败,尝试按文本格式导入: {exc}") return parse_accounts_text(text) return accounts_from_payload(data, source) def accounts_from_payload(data: Any, source: str = "导入数据") -> list[Account]: """从结构化数据中提取账号列表。""" if isinstance(data, Mapping): rows = data.get("accounts") or data.get("data") or data.get("items") or [] else: rows = data if not isinstance(rows, list): logger.warning(f"{source} 中未找到账号列表") return [] return accounts_from_rows(rows, source) def accounts_from_rows(rows: Iterable[Any], source: str = "导入数据") -> list[Account]: """从字典行列表转换账号。""" accounts: list[Account] = [] for row_index, row in enumerate(rows, 1): if not isinstance(row, Mapping): logger.warning(f"{source} 第{row_index}行不是对象,已跳过") continue account = _account_from_mapping(row, f"{source} 第{row_index}行") if account: accounts.append(account) return accounts def _parse_csv_text(text: str, source: str) -> list[Account]: """解析带表头的CSV文件。""" reader = csv.reader(io.StringIO(text)) try: first_row = next(reader) except StopIteration: return [] if not _looks_like_header(first_row): return [] dict_reader = csv.DictReader(io.StringIO(text)) return accounts_from_rows(dict_reader, source) def _looks_like_header(row: list[str]) -> bool: """判断CSV首行是否像账号字段表头。""" normalized = { "".join(str(value).lower().replace("-", "_").split()) for value in row } return len(normalized & set(NORMALIZED_ALIASES.keys())) >= 2 def _account_from_mapping(row: Mapping[str, Any], source: str) -> Account | None: """从字典字段构造账号。""" normalized_row: dict[str, str] = {} for key, value in row.items(): normalized_key = "".join(str(key).lower().replace("-", "_").split()) field = NORMALIZED_ALIASES.get(normalized_key) if field: normalized_row[field] = "" if value is None else str(value).strip() username = normalized_row.get("username", "") password = normalized_row.get("password", "") email = normalized_row.get("email", "") email_password = normalized_row.get("email_password", "") if not all([username, password, email, email_password]): logger.warning(f"{source} 存在空字段,已跳过") return None if not EMAIL_PATTERN.match(email) or not _is_ascii(email): logger.warning(f"{source} 邮箱格式不正确: {email}") return None if not _is_ascii(email_password): logger.warning(f"{source} 邮箱密码/授权码包含非ASCII字符,IMAP可能无法登录") return None email_config = get_email_config_for_account(email) imap_server = normalized_row.get("email_imap_server") or email_config["server"] try: imap_port = int(normalized_row.get("email_imap_port") or email_config["port"]) except ValueError: logger.warning(f"{source} IMAP端口不正确,已使用默认端口") imap_port = email_config["port"] return Account( username=username, password=password, email=email, email_password=email_password, email_imap_server=imap_server, email_imap_port=imap_port, )