Files
live-hub-py/gui/account_importer.py
T
2026-06-22 10:10:51 +08:00

186 lines
5.8 KiB
Python

"""账号导入工具。"""
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,
)