彻底优化项目结构,使用 web
This commit is contained in:
+17
-4
@@ -1,4 +1,17 @@
|
||||
.venv
|
||||
*/__pycache__
|
||||
/__pycache__
|
||||
data/gui_state.json
|
||||
.venv/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# 运行时数据
|
||||
data/
|
||||
logs/
|
||||
*.db
|
||||
|
||||
# geetest 临时图片
|
||||
bg.jpg
|
||||
fullbg.jpg
|
||||
slice.jpg
|
||||
|
||||
# 前端
|
||||
web/frontend/node_modules/
|
||||
web/frontend/dist/
|
||||
|
||||
@@ -1,144 +1,84 @@
|
||||
# 斗鱼自动登录工具 - Python版
|
||||
# 斗鱼批量登录 Web 后台
|
||||
|
||||
全自动登录斗鱼账号,获取Cookie。
|
||||
全自动登录斗鱼账号、获取 Cookie 的 Web 管理平台。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- ✅ 自动过极验v3滑块验证
|
||||
- ✅ 自动获取邮箱验证码(IMAP)
|
||||
- ✅ 批量账号登录
|
||||
- ✅ Cookie持久化存储
|
||||
- ✅ 代理支持
|
||||
- ✅ GUI导入账号、自动保存配置、代理测试
|
||||
- 极验 v3 滑块自动识别
|
||||
- 邮箱验证码自动获取(IMAP)
|
||||
- 批量登录 + 实时日志推送(WebSocket)
|
||||
- 代理 & 白名单自动管理
|
||||
- 角色权限控制(超级管理员 / 运营 / 客服)
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
douyu_login_py/
|
||||
├── main.py # 主入口
|
||||
├── config.yaml # 配置文件
|
||||
├── pyproject.toml # uv项目配置
|
||||
├── douyu/ # 斗鱼登录模块
|
||||
│ ├── config.py # 配置管理
|
||||
│ ├── crypto.py # 加密工具
|
||||
│ ├── email_verifier.py # 邮箱验证
|
||||
│ └── login.py # 登录核心
|
||||
├── geetest/ # 极验滑块破解(复用geetest-v3-silde-crack)
|
||||
│ ├── solver.py
|
||||
│ ├── network.py
|
||||
│ ├── crypto.py
|
||||
│ ├── imaging.py
|
||||
│ └── trajectory.py
|
||||
├── utils/ # 工具模块
|
||||
│ ├── logger.py
|
||||
│ └── helpers.py
|
||||
└── data/
|
||||
├── accounts.json # 账号配置
|
||||
└── cookies/ # Cookie存储
|
||||
├── core/ # 核心业务逻辑
|
||||
│ ├── models.py # Account, ProxyConfig 数据类
|
||||
│ ├── douyu/ # 斗鱼登录模块
|
||||
│ │ ├── login.py # 登录流程
|
||||
│ │ ├── crypto.py # 密码加密
|
||||
│ │ ├── email_verifier.py # 邮箱验证码
|
||||
│ │ ├── proxy.py # 代理管理
|
||||
│ │ └── whitelist.py # 白名单管理
|
||||
│ └── geetest/ # 极验验证码求解
|
||||
│ ├── common/ # 公共工具(网络/加密/图像/轨迹)
|
||||
│ └── v3_slide/ # v3 滑块求解器
|
||||
├── web/ # Web 后台
|
||||
│ ├── backend/ # FastAPI 后端
|
||||
│ │ ├── main.py # 入口
|
||||
│ │ ├── routers/ # API 路由
|
||||
│ │ └── services/ # 业务服务
|
||||
│ └── frontend/ # React + Ant Design 前端
|
||||
├── utils/ # 通用工具
|
||||
│ └── logger.py
|
||||
├── data/ # 运行时数据(gitignore)
|
||||
├── start_web.sh # 一键启动
|
||||
└── pyproject.toml
|
||||
```
|
||||
|
||||
## 安装依赖
|
||||
## 快速开始
|
||||
|
||||
```bash
|
||||
uv sync
|
||||
# 一键启动(自动检查依赖)
|
||||
./start_web.sh
|
||||
```
|
||||
|
||||
## GUI使用
|
||||
|
||||
GUI不再依赖 `config.yaml` 启动,账号数据在界面里导入,代理、极验重试、日志等级和账号列表会自动保存到 `data/gui_state.json`。
|
||||
或手动启动:
|
||||
|
||||
```bash
|
||||
uv run python main.py --gui
|
||||
# 后端
|
||||
uv pip install -e .
|
||||
.venv/bin/python -m uvicorn web.backend.main:app --reload --port 8000
|
||||
|
||||
# 前端
|
||||
cd web/frontend && npm install && npm run dev
|
||||
```
|
||||
|
||||
支持导入格式:
|
||||
访问 `http://localhost:5173`,默认账号 `admin / admin123`。
|
||||
|
||||
```text
|
||||
用户名|密码|邮箱|邮箱密码
|
||||
```
|
||||
## 角色权限
|
||||
|
||||
也可以导入 `txt`、`csv`、`json`、`yaml` 文件。旧版 `config.yaml` 可直接在 GUI 中作为 YAML 文件导入,用来迁移账号数据。
|
||||
|
||||
代理区域支持 API 获取和静态代理,填写后可以点击“测试代理”验证出口连通性。开始批量登录时也会先进行代理预检,只有拿到可用出口后才会继续登录流程。
|
||||
|
||||
## 命令行配置
|
||||
|
||||
命令行模式仍使用 `config.yaml`:
|
||||
|
||||
```yaml
|
||||
accounts:
|
||||
- username: "your_username"
|
||||
password: "your_password"
|
||||
email: "your_email@bdhg.xyz"
|
||||
email_password: "your_email_password"
|
||||
email_imap_server: "mail.bdhg.xyz"
|
||||
email_imap_port: 993
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 单账号登录
|
||||
|
||||
```bash
|
||||
# 登录第一个账号
|
||||
uv run python main.py
|
||||
|
||||
# 登录指定索引的账号
|
||||
uv run python main.py -i 0
|
||||
```
|
||||
|
||||
### 批量登录
|
||||
|
||||
```bash
|
||||
uv run python main.py --batch
|
||||
```
|
||||
|
||||
### 详细日志
|
||||
|
||||
```bash
|
||||
uv run python main.py -v
|
||||
```
|
||||
| 角色 | 权限 |
|
||||
|---|---|
|
||||
| 超级管理员 | 全部功能 + 用户管理 + 权限分配 |
|
||||
| 运营 | 账号管理 + 批量登录 + 代理配置 + Cookie 导出 |
|
||||
| 客服 | 仅查看分配给自己的账号(只显示用户名) |
|
||||
|
||||
## 登录流程
|
||||
|
||||
```
|
||||
1️⃣ 第一次登录 → 获取极验参数
|
||||
2️⃣ 极验滑块验证 → 自动识别缺口位置
|
||||
3️⃣ 第二次登录 → 获取邮箱验证code
|
||||
4️⃣ 发送邮箱验证邮件
|
||||
5️⃣ IMAP获取验证码
|
||||
6️⃣ 提交验证码
|
||||
7️⃣ 完成登录获取Cookie
|
||||
1. 获取极验参数 → 滑块验证 → 自动识别缺口
|
||||
2. 获取邮箱验证 code → 发送验证邮件
|
||||
3. IMAP 获取验证码 → 提交验证码 → 获取 Cookie
|
||||
```
|
||||
|
||||
## Cookie使用
|
||||
## 技术栈
|
||||
|
||||
登录成功后,Cookie会保存到 `data/cookies/` 目录。
|
||||
|
||||
可以使用以下方式读取Cookie:
|
||||
|
||||
```python
|
||||
from utils.helpers import load_cookie
|
||||
|
||||
cookie = load_cookie("your_username")
|
||||
print(cookie)
|
||||
```
|
||||
|
||||
## 极验滑块破解
|
||||
|
||||
本项目复用了 `geetest-v3-silde-crack` 的滑块破解方案,包含:
|
||||
|
||||
- RSA/AES加密
|
||||
- 轨迹生成
|
||||
- 图像识别(OpenCV)
|
||||
- 性能数据伪造
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 请确保邮箱支持IMAP协议
|
||||
2. 邮箱需要开启IMAP访问权限
|
||||
3. 部分邮箱需要使用应用专用密码
|
||||
4. 建议使用代理避免IP限制
|
||||
- 后端:FastAPI + SQLAlchemy + SQLite + JWT
|
||||
- 前端:React + Ant Design + Vite
|
||||
- 核心:Python + OpenCV + pycryptodome
|
||||
|
||||
## 许可证
|
||||
|
||||
|
||||
Binary file not shown.
@@ -1,14 +0,0 @@
|
||||
软软马卡龙(1)
|
||||
|
||||
cvl_csrf_token=62083f188904477e9bc1c5a70537145c; acf_ccn=8b025ffcbf403d5e0d880ad6327bb3cf; PHPSESSID=86k3denvoc1j16rl1fsv5l2mc2; acf_auth=5a8eUzBoDc8uyLasCP7LXaO9a2vUK%2FDaNFwYi0MVdUFMpUcMBfsoZfifJZKCQC8WgJuKq7WB78VZrRlRXnZUSt87TZM0tLNdnplqtuL87c7idAqo%2Fal57ls; acf_jwt_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJtZDUifQ.eyJ1aWQiOjk0MjQxNjQ1MSwiY3QiOjAsInN1YiI6InN0IiwiYXVkIjpbImR5Iiwibm9uZSJdLCJiaXoiOjEsImx0a2lkIjo2OTE2MTY4NSwic3RrIjoiNGZkNDY5ZDE2OWY3NzM0YiIsImV4cCI6MTc4MjMyMjc2OSwiaWF0IjoxNzgxNzE3OTcwLCJrZXkiOiJkeS1qd3QtbWQ1In0.NDc0YzMyNjQ0OGIzNTkxYzkxNGQ1NDk0Y2U0NTk1NTU; acf_dmjwt_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJtZDUifQ.eyJ1aWQiOjk0MjQxNjQ1MSwiY3QiOjAsInN1YiI6InN0IiwiYXVkIjpbImRtIiwibm9uZSJdLCJiaXoiOjEsImx0a2lkIjo2OTE2MTY4NSwic3RrIjoiNGZkNDY5ZDE2OWY3NzM0YiIsImV4cCI6MTc4MjMyMjc2OSwiaWF0IjoxNzgxNzE3OTcwLCJrZXkiOiJkeS1qd3QtbWQ1In0.NWM1ZDgwZjE1N2ZhNTE3NDUxNmQ4ZjljMzI5ODQ0ZjM; dy_auth=29bfEdmmHp50M9Zr2gxQ3%2F2AJD2DAdCK6I%2FoX92U5n13i64R%2B%2FywJZf%2FqG4%2BCJvbUQmFaUSu3dFbJj4%2Be1gU1DpW6ligJYaJLnNuVbmKMeZCtayBmGtns6A; wan_auth37wan=cf514c616a0fzsvhClbvRbju0sSZb4aDqHQQabpd66aLoVky4bvRRXYDfYMX3iSUNiDuh%2BHt%2B20QXW0f5JLZ6lBtOW8LIIwb31iX8OiZurGB0aztSKA; acf_uid=942416451; acf_username=942416451; acf_nickname=%E7%94%A8%E6%88%B74512197651; acf_own_room=0; acf_groupid=1; acf_phonestatus=1; acf_avatar=https%3A%2F%2Fapic.douyucdn.cn%2Fupload%2Favatar%2Fdefault%2F18_; acf_ct=0; acf_ltkid=69161685; acf_biz=1; acf_stk=4fd469d169f7734b; acf_devid=9869cce23d84da0fd8b1cd20162b3def--用户4512197651--
|
||||
|
||||
|
||||
============
|
||||
|
||||
新写的
|
||||
|
||||
dy_accounts_main=; dy_auth=e105BJzhxVWQ0tJKUXiiqN8ky11wgfj7ClW%2F8qo%2B1MPv8%2BDGHBq8VnuaJ1MWnm4EZqgVfLkM6PETWtGpLAHvadcMxCn9HWvTfT8%2BSKZR2dmsvReubJmeqBo; wan_auth37wan=4dc825c71d00LrMvodf%2B7ZrhnyMcsREgwQlgd7Ec06k5%2Bm%2FM0tdv0AJJ8gpoNoinSoPHS3vbJO8iK1vqiXfF2hTOLQqJOW3Wzs19DWLwpJpQgV8J%2BF0; LTP0=eyJhbGciOiJtZDUiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOlsicGFzc3BvcnQiXSwiY3QiOjAsImN0aW1lIjoxNzgyMDU0MDA0LCJleHAiOjE3OTc4MjIwMTYsImtleSI6ImR5LWp3dC1tZDUiLCJsdGsiOiJhZDMxOTliZGRhYmM3ZWRmIiwibHRraWQiOjgwNTYxMzgyLCJzdWIiOiJsdCIsInVpZCI6OTg2Mzg1Mjk2fQ.Y2M0YTQ4YjQwZGYwZWI4YWZiZTllNmVjODNiZmJkMGE; last_login_way=nickname; PHPSESSID=ccr09aa2letju5662gdh5aih62; acf_auth=1a34kGp6rHjazzRy4fVzKXX11qCjC9RuQASDGOEfkRbLjsI3A5PzSCXawo5hZvO4g47LF9ruapNiMpf%2BE3u4TplWPMkW4c4eI0pyG05a%2F9dKr%2BOGM0hAEXk; acf_jwt_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJtZDUifQ.eyJ1aWQiOjk4NjM4NTI5NiwiY3QiOjAsInN1YiI6InN0IiwiYXVkIjpbImR5Iiwibm9uZSJdLCJiaXoiOjEsImx0a2lkIjo4MDU2MTM4Miwic3RrIjoiNjFhODczZjUwZTg3M2Y5MCIsImV4cCI6MTc4MjY1ODgwNCwiaWF0IjoxNzgyMDU0MDA0LCJrZXkiOiJkeS1qd3QtbWQ1In0.OTFlMjIxOGU4MjZkMjcwMWM4NjQ1NTcwNjNmZWIzMDA; acf_dmjwt_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJtZDUifQ.eyJ1aWQiOjk4NjM4NTI5NiwiY3QiOjAsInN1YiI6InN0IiwiYXVkIjpbImRtIiwibm9uZSJdLCJiaXoiOjEsImx0a2lkIjo4MDU2MTM4Miwic3RrIjoiNjFhODczZjUwZTg3M2Y5MCIsImV4cCI6MTc4MjY1ODgwNCwiaWF0IjoxNzgyMDU0MDA0LCJrZXkiOiJkeS1qd3QtbWQ1In0.N2E0NmEyOWVlZDE1NTE4NDg0MTFlMmM5OGY1ZGQzZTk; acf_uid=986385296; acf_username=986385296; acf_nickname=%E7%94%A8%E6%88%B79006787894; acf_own_room=0; acf_groupid=1; acf_phonestatus=1; acf_avatar=https%3A%2F%2Fapic.douyucdn.cn%2Fupload%2Favatar%2Fdefault%2F25_; acf_ct=0; acf_ltkid=80561382; acf_biz=1; acf_stk=61a873f50e873f90; acf_devid=d53498d383f449f4c4ea28c4165aa2b4",
|
||||
|
||||
|
||||
|
||||
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
# 斗鱼自动登录配置
|
||||
|
||||
# 账号配置
|
||||
accounts:
|
||||
- username: "用户9006787894"
|
||||
password: "a778899"
|
||||
email: "jtmjcm@bdhg.xyz"
|
||||
email_password: "www123"
|
||||
email_imap_server: "mail.bdhg.xyz"
|
||||
email_imap_port: 993
|
||||
|
||||
# 代理配置
|
||||
proxy:
|
||||
enabled: true
|
||||
# 代理API地址(自动获取代理IP)
|
||||
api_url: "http://api.xiequ.cn/VAD/GetIp.aspx?act=get&uid=106015&vkey=97111DB5379E38E3BC2FF09A1B00A0C7&num=1&time=30&plat=1&re=0&type=0&so=1&ow=1&spl=1&addr=&db=1"
|
||||
# 静态代理(如果不使用API获取)
|
||||
http: ""
|
||||
https: ""
|
||||
|
||||
# 极验配置
|
||||
geetest:
|
||||
# 最大重试次数(遇到点选验证码时重试)
|
||||
max_retries: 10
|
||||
|
||||
# 日志配置
|
||||
log:
|
||||
level: "DEBUG"
|
||||
file: "logs/douyu_login.log"
|
||||
|
||||
# Cookie存储
|
||||
cookie_dir: "data/cookies"
|
||||
@@ -0,0 +1,5 @@
|
||||
"""核心业务逻辑"""
|
||||
|
||||
from .models import Account, ProxyConfig
|
||||
|
||||
__all__ = ["Account", "ProxyConfig"]
|
||||
@@ -2,8 +2,7 @@
|
||||
|
||||
from .login import DouyuLogin
|
||||
from .email_verifier import EmailVerifier
|
||||
from .config import Config
|
||||
from .proxy import ProxyManager
|
||||
from .whitelist import WhitelistManager
|
||||
|
||||
__all__ = ["DouyuLogin", "EmailVerifier", "Config", "ProxyManager", "WhitelistManager"]
|
||||
__all__ = ["DouyuLogin", "EmailVerifier", "ProxyManager", "WhitelistManager"]
|
||||
@@ -5,23 +5,19 @@ import json
|
||||
import time
|
||||
import requests
|
||||
from typing import Mapping, Optional, Tuple
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlsplit, urlunsplit
|
||||
from loguru import logger
|
||||
|
||||
from .config import Account
|
||||
from core.models import Account
|
||||
from .crypto import encrypt_password, encrypt_nickname_or_phone
|
||||
from .email_verifier import EmailVerifier
|
||||
from .proxy import ProxyManager, get_proxy_manager
|
||||
|
||||
# 导入geetest滑块模块
|
||||
import sys
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
from geetest import run_solver
|
||||
from geetest.solver import (
|
||||
from core.geetest import run_solver
|
||||
from core.geetest.v3_slide.solver import (
|
||||
_generate_seed, get_w1, get_w2,
|
||||
)
|
||||
from geetest.network import (
|
||||
from core.geetest.common.network import (
|
||||
get_js_address,
|
||||
get_c_s,
|
||||
req_fullpage_validate,
|
||||
@@ -0,0 +1,5 @@
|
||||
"""Geetest 验证码求解器"""
|
||||
|
||||
from .v3_slide.solver import run_solver
|
||||
|
||||
__all__ = ["run_solver"]
|
||||
@@ -0,0 +1 @@
|
||||
"""Geetest 公共工具模块"""
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Geetest slider captcha solver package."""
|
||||
"""Geetest v3 滑块验证码求解器"""
|
||||
|
||||
from .solver import run_solver
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import time
|
||||
import random
|
||||
import json
|
||||
from .trajectory import generate_realistic_trajectory, process_mouse_trajectory, compress_trajectory, TrajectoryEncoder, \
|
||||
from core.geetest.common.trajectory import generate_realistic_trajectory, process_mouse_trajectory, compress_trajectory, TrajectoryEncoder, \
|
||||
H
|
||||
from .crypto import four_random_chart, RSA_jiami_r, AES_O, geetest_base64_encode, encrypt_string, simple_md5
|
||||
from .imaging import download_picture
|
||||
from .network import get_challenge_gt, get_js_address, get_c_s, req_slide, get_picture, req_end
|
||||
from .performance import generate_fake_performance_timing, get_slide_track
|
||||
from core.geetest.common.crypto import four_random_chart, RSA_jiami_r, AES_O, geetest_base64_encode, encrypt_string, simple_md5
|
||||
from core.geetest.common.imaging import download_picture
|
||||
from core.geetest.common.network import get_challenge_gt, get_js_address, get_c_s, req_slide, get_picture, req_end
|
||||
from core.geetest.common.performance import generate_fake_performance_timing, get_slide_track
|
||||
|
||||
|
||||
def _generate_seed() -> str:
|
||||
@@ -0,0 +1,27 @@
|
||||
"""核心数据模型"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Account:
|
||||
"""账号配置"""
|
||||
username: str
|
||||
password: str
|
||||
email: str
|
||||
email_password: str
|
||||
email_imap_server: str
|
||||
email_imap_port: int = 993
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProxyConfig:
|
||||
"""代理配置"""
|
||||
enabled: bool = False
|
||||
api_url: str = ""
|
||||
http: str = ""
|
||||
https: str = ""
|
||||
# 白名单配置
|
||||
whitelist_enabled: bool = False
|
||||
whitelist_uid: str = ""
|
||||
whitelist_ukey: str = ""
|
||||
BIN
Binary file not shown.
@@ -1,85 +0,0 @@
|
||||
"""配置管理模块"""
|
||||
|
||||
import yaml
|
||||
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:
|
||||
"""账号配置"""
|
||||
username: str
|
||||
password: str
|
||||
email: str
|
||||
email_password: str
|
||||
email_imap_server: str
|
||||
email_imap_port: int = 993
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProxyConfig:
|
||||
"""代理配置"""
|
||||
enabled: bool = False
|
||||
api_url: str = ""
|
||||
http: str = ""
|
||||
https: str = ""
|
||||
# 白名单配置
|
||||
whitelist_enabled: bool = False # 是否启用白名单自动管理
|
||||
whitelist_uid: str = "" # 协固用户ID
|
||||
whitelist_ukey: str = "" # 协固用户密钥
|
||||
|
||||
|
||||
class Config:
|
||||
"""配置管理器"""
|
||||
|
||||
def __init__(self, config_path: str = "config.yaml"):
|
||||
self.config_path = Path(config_path)
|
||||
self._config = self._load_config()
|
||||
|
||||
def _load_config(self) -> dict:
|
||||
"""加载配置文件"""
|
||||
if not self.config_path.exists():
|
||||
raise FileNotFoundError(f"配置文件不存在: {self.config_path}")
|
||||
|
||||
with open(self.config_path, 'r', encoding='utf-8') as f:
|
||||
return yaml.safe_load(f)
|
||||
|
||||
def get_accounts(self) -> List[Account]:
|
||||
"""获取账号列表"""
|
||||
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') or email_config['server'],
|
||||
email_imap_port=acc.get('email_imap_port') or email_config['port'],
|
||||
))
|
||||
return accounts
|
||||
|
||||
def get_proxy(self) -> ProxyConfig:
|
||||
"""获取代理配置"""
|
||||
proxy = self._config.get('proxy', {})
|
||||
return ProxyConfig(
|
||||
enabled=proxy.get('enabled', False),
|
||||
api_url=proxy.get('api_url', ''),
|
||||
http=proxy.get('http', ''),
|
||||
https=proxy.get('https', ''),
|
||||
)
|
||||
|
||||
def get_geetest_config(self) -> dict:
|
||||
"""获取极验配置"""
|
||||
return self._config.get('geetest', {'max_retries': 5})
|
||||
|
||||
def get_cookie_dir(self) -> str:
|
||||
"""获取Cookie存储目录"""
|
||||
return self._config.get('cookie_dir', 'data/cookies')
|
||||
|
||||
def get_log_config(self) -> dict:
|
||||
"""获取日志配置"""
|
||||
return self._config.get('log', {'level': 'INFO', 'file': 'logs/douyu_login.log'})
|
||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 6.2 KiB |
@@ -1,13 +0,0 @@
|
||||
"""GUI模块"""
|
||||
|
||||
from .app import DouyuLoginApp
|
||||
from .login_worker import parse_accounts_text, get_imap_server
|
||||
from .widgets import AccountTable, LogPanel
|
||||
|
||||
__all__ = [
|
||||
'DouyuLoginApp',
|
||||
'parse_accounts_text',
|
||||
'get_imap_server',
|
||||
'AccountTable',
|
||||
'LogPanel',
|
||||
]
|
||||
@@ -1,185 +0,0 @@
|
||||
"""账号导入工具。"""
|
||||
|
||||
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,
|
||||
)
|
||||
-1507
File diff suppressed because it is too large
Load Diff
@@ -1,203 +0,0 @@
|
||||
"""登录工作线程模块"""
|
||||
|
||||
import csv
|
||||
import re
|
||||
import threading
|
||||
import time
|
||||
from queue import Queue
|
||||
from typing import Optional
|
||||
from loguru import logger
|
||||
|
||||
from douyu import DouyuLogin
|
||||
from douyu.config import Account, ProxyConfig
|
||||
from douyu.email_verifier import get_email_config_for_account
|
||||
|
||||
|
||||
EMAIL_PATTERN = re.compile(r'^[^\s@|]+@[^\s@|]+\.[^\s@|]+$')
|
||||
HEADER_NAMES = {
|
||||
'username', 'user', 'account', '账号', '用户名', '斗鱼账号',
|
||||
'password', 'pass', 'pwd', '密码', '登录密码',
|
||||
'email', 'mail', '邮箱', '邮箱地址',
|
||||
'email_password', 'email_pass', 'email_pwd', 'mail_password',
|
||||
'mail_pass', '邮箱密码', '邮箱授权码', '授权码',
|
||||
}
|
||||
|
||||
|
||||
def get_imap_server(email: str) -> str:
|
||||
"""从邮箱地址推导IMAP服务器"""
|
||||
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 _split_account_line(line: str) -> list[str]:
|
||||
"""拆分单行账号数据,支持竖线、Tab和CSV逗号。"""
|
||||
if '|' in line:
|
||||
return line.split('|')
|
||||
if '\t' in line:
|
||||
return line.split('\t')
|
||||
if ',' in line:
|
||||
return next(csv.reader([line]))
|
||||
return line.split()
|
||||
|
||||
|
||||
def _looks_like_header(parts: list[str]) -> bool:
|
||||
"""判断一行是否像表头。"""
|
||||
normalized = {
|
||||
part.strip().lower().replace('-', '_')
|
||||
for part in parts
|
||||
if part.strip()
|
||||
}
|
||||
return len(normalized & HEADER_NAMES) >= 2
|
||||
|
||||
|
||||
def parse_accounts_text(text: str) -> list[Account]:
|
||||
"""
|
||||
解析账号文本
|
||||
|
||||
格式:用户名|密码|邮箱|邮箱密码
|
||||
|
||||
Returns:
|
||||
Account列表
|
||||
"""
|
||||
accounts = []
|
||||
|
||||
for line_num, line in enumerate(text.strip().split('\n'), 1):
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
|
||||
parts = _split_account_line(line)
|
||||
if line_num == 1 and _looks_like_header(parts):
|
||||
continue
|
||||
|
||||
if len(parts) != 4:
|
||||
logger.warning(f"第{line_num}行格式错误,需要4个字段,实际{len(parts)}个: {line}")
|
||||
continue
|
||||
|
||||
username, password, email, email_password = parts[0].strip(), parts[1].strip(), parts[2].strip(), parts[3].strip()
|
||||
|
||||
if not all([username, password, email, email_password]):
|
||||
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,
|
||||
email=email,
|
||||
email_password=email_password,
|
||||
email_imap_server=get_imap_server(email),
|
||||
email_imap_port=993,
|
||||
))
|
||||
|
||||
return accounts
|
||||
|
||||
|
||||
class LoginWorker(threading.Thread):
|
||||
"""登录工作线程"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
account: Account,
|
||||
index: int,
|
||||
result_queue: Queue,
|
||||
log_queue: Queue,
|
||||
proxy_config: Optional[ProxyConfig] = None,
|
||||
max_geetest_retries: int = 5,
|
||||
):
|
||||
super().__init__(daemon=True)
|
||||
self.account = account
|
||||
self.index = index
|
||||
self.result_queue = result_queue
|
||||
self.log_queue = log_queue
|
||||
self.proxy_config = proxy_config
|
||||
self.max_geetest_retries = max_geetest_retries
|
||||
self._stop_event = threading.Event()
|
||||
|
||||
def stop(self):
|
||||
"""停止线程"""
|
||||
self._stop_event.set()
|
||||
|
||||
def run(self):
|
||||
"""执行登录"""
|
||||
username = self.account.username
|
||||
|
||||
try:
|
||||
# 发送日志
|
||||
self.log_queue.put(('info', f'[{self.index + 1}] 开始登录: {username}'))
|
||||
|
||||
# 配置代理
|
||||
proxy_url = None
|
||||
proxy_api_url = None
|
||||
|
||||
if self.proxy_config and self.proxy_config.enabled:
|
||||
if self.proxy_config.http or self.proxy_config.https:
|
||||
# 优先使用GUI预检通过的代理,避免进入登录流程后再直接获取未验证代理。
|
||||
proxy_url = {
|
||||
'http': self.proxy_config.http or self.proxy_config.https,
|
||||
'https': self.proxy_config.https or self.proxy_config.http,
|
||||
}
|
||||
elif self.proxy_config.api_url:
|
||||
proxy_api_url = self.proxy_config.api_url
|
||||
|
||||
# 创建登录器
|
||||
loginer = DouyuLogin(
|
||||
self.account,
|
||||
proxy=proxy_url,
|
||||
proxy_api_url=proxy_api_url,
|
||||
max_geetest_retries=self.max_geetest_retries,
|
||||
)
|
||||
|
||||
# 执行登录
|
||||
result = loginer.login()
|
||||
|
||||
if result.success:
|
||||
self.log_queue.put(('success', f'[{self.index + 1}] {username} 登录成功'))
|
||||
self.result_queue.put({
|
||||
'index': self.index,
|
||||
'username': username,
|
||||
'email': self.account.email,
|
||||
'status': 'success',
|
||||
'cookie': result.cookie,
|
||||
'message': '登录成功',
|
||||
})
|
||||
else:
|
||||
self.log_queue.put(('error', f'[{self.index + 1}] {username} 登录失败: {result.message}'))
|
||||
self.result_queue.put({
|
||||
'index': self.index,
|
||||
'username': username,
|
||||
'email': self.account.email,
|
||||
'status': 'failed',
|
||||
'cookie': '',
|
||||
'message': result.message,
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
self.log_queue.put(('error', f'[{self.index + 1}] {username} 登录异常: {error_msg}'))
|
||||
self.result_queue.put({
|
||||
'index': self.index,
|
||||
'username': username,
|
||||
'email': self.account.email,
|
||||
'status': 'error',
|
||||
'cookie': '',
|
||||
'message': error_msg,
|
||||
})
|
||||
-118
@@ -1,118 +0,0 @@
|
||||
"""GUI本地状态存储。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from douyu.config import Account, ProxyConfig
|
||||
from douyu.email_verifier import get_email_config_for_account
|
||||
|
||||
|
||||
DEFAULT_STATE_PATH = Path("data/gui_state.json")
|
||||
|
||||
|
||||
def account_to_dict(account: Account) -> dict[str, Any]:
|
||||
"""把账号对象转换成可保存的字典。"""
|
||||
return {
|
||||
"username": account.username,
|
||||
"password": account.password,
|
||||
"email": account.email,
|
||||
"email_password": account.email_password,
|
||||
"email_imap_server": account.email_imap_server,
|
||||
"email_imap_port": account.email_imap_port,
|
||||
}
|
||||
|
||||
|
||||
def account_from_dict(data: dict[str, Any]) -> Account | None:
|
||||
"""从本地状态恢复账号对象。"""
|
||||
username = str(data.get("username", "")).strip()
|
||||
password = str(data.get("password", "")).strip()
|
||||
email = str(data.get("email", "")).strip()
|
||||
email_password = str(data.get("email_password", "")).strip()
|
||||
|
||||
if not all([username, password, email, email_password]):
|
||||
return None
|
||||
|
||||
email_config = get_email_config_for_account(email)
|
||||
imap_server = str(data.get("email_imap_server") or email_config["server"]).strip()
|
||||
try:
|
||||
imap_port = int(data.get("email_imap_port") or email_config["port"])
|
||||
except (TypeError, ValueError):
|
||||
imap_port = int(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,
|
||||
)
|
||||
|
||||
|
||||
class GuiStateStore:
|
||||
"""负责保存和读取GUI状态。"""
|
||||
|
||||
def __init__(self, path: str | Path = DEFAULT_STATE_PATH):
|
||||
self.path = Path(path)
|
||||
|
||||
def load(self) -> dict[str, Any]:
|
||||
"""读取本地状态文件。"""
|
||||
if not self.path.exists():
|
||||
return {}
|
||||
|
||||
try:
|
||||
with self.path.open("r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
return data if isinstance(data, dict) else {}
|
||||
except Exception as exc:
|
||||
logger.warning(f"读取GUI状态失败,将使用默认值: {exc}")
|
||||
return {}
|
||||
|
||||
def save(self, state: dict[str, Any]) -> None:
|
||||
"""原子写入本地状态文件。"""
|
||||
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp_path = self.path.with_suffix(f"{self.path.suffix}.tmp")
|
||||
|
||||
with tmp_path.open("w", encoding="utf-8") as f:
|
||||
json.dump(state, f, ensure_ascii=False, indent=2)
|
||||
|
||||
tmp_path.replace(self.path)
|
||||
|
||||
@staticmethod
|
||||
def accounts_from_state(state: dict[str, Any]) -> list[Account]:
|
||||
"""从状态字典恢复账号列表。"""
|
||||
accounts = []
|
||||
raw_accounts = state.get("accounts", [])
|
||||
if not isinstance(raw_accounts, list):
|
||||
return accounts
|
||||
|
||||
for raw_account in raw_accounts:
|
||||
if not isinstance(raw_account, dict):
|
||||
continue
|
||||
account = account_from_dict(raw_account)
|
||||
if account:
|
||||
accounts.append(account)
|
||||
|
||||
return accounts
|
||||
|
||||
@staticmethod
|
||||
def proxy_from_state(state: dict[str, Any]) -> ProxyConfig:
|
||||
"""从状态字典恢复代理配置。"""
|
||||
proxy = state.get("proxy", {})
|
||||
if not isinstance(proxy, dict):
|
||||
return ProxyConfig()
|
||||
|
||||
return ProxyConfig(
|
||||
enabled=bool(proxy.get("enabled", False)),
|
||||
api_url=str(proxy.get("api_url", "")).strip(),
|
||||
http=str(proxy.get("http", "")).strip(),
|
||||
https=str(proxy.get("https", "")).strip(),
|
||||
whitelist_enabled=bool(proxy.get("whitelist_enabled", False)),
|
||||
whitelist_uid=str(proxy.get("whitelist_uid", "")).strip(),
|
||||
whitelist_ukey=str(proxy.get("whitelist_ukey", "")).strip(),
|
||||
)
|
||||
-212
@@ -1,212 +0,0 @@
|
||||
"""自定义GUI组件"""
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class AccountTable(ttk.Treeview):
|
||||
"""账号列表表格"""
|
||||
|
||||
COLUMNS = ('index', 'username', 'email', 'status', 'cookie')
|
||||
|
||||
def __init__(self, parent, **kwargs):
|
||||
super().__init__(
|
||||
parent,
|
||||
columns=self.COLUMNS,
|
||||
show='headings',
|
||||
selectmode='extended',
|
||||
**kwargs
|
||||
)
|
||||
|
||||
# 设置列标题
|
||||
self.heading('index', text='序号')
|
||||
self.heading('username', text='用户名')
|
||||
self.heading('email', text='邮箱')
|
||||
self.heading('status', text='状态')
|
||||
self.heading('cookie', text='Cookie')
|
||||
|
||||
# 设置列宽
|
||||
self.column('index', width=50, minwidth=50, anchor='center')
|
||||
self.column('username', width=150, minwidth=100)
|
||||
self.column('email', width=200, minwidth=150)
|
||||
self.column('status', width=80, minwidth=60, anchor='center')
|
||||
self.column('cookie', width=200, minwidth=100)
|
||||
|
||||
# 添加滚动条
|
||||
scrollbar = ttk.Scrollbar(parent, orient='vertical', command=self.yview)
|
||||
self.configure(yscrollcommand=scrollbar.set)
|
||||
scrollbar.pack(side='right', fill='y')
|
||||
|
||||
# 状态标签样式
|
||||
self.tag_configure('pending', foreground='gray')
|
||||
self.tag_configure('running', foreground='blue')
|
||||
self.tag_configure('success', foreground='green')
|
||||
self.tag_configure('failed', foreground='red')
|
||||
self.tag_configure('error', foreground='red')
|
||||
|
||||
def add_account(self, index: int, username: str, email: str):
|
||||
"""添加账号到表格"""
|
||||
self.insert('', 'end', iid=str(index), values=(
|
||||
index + 1,
|
||||
username,
|
||||
email,
|
||||
'待登录',
|
||||
'-'
|
||||
), tags=('pending',))
|
||||
|
||||
def update_status(self, index: int, status: str, cookie: str = ''):
|
||||
"""更新账号状态"""
|
||||
status_map = {
|
||||
'pending': ('待登录', 'pending'),
|
||||
'running': ('登录中...', 'running'),
|
||||
'success': ('成功', 'success'),
|
||||
'failed': ('失败', 'failed'),
|
||||
'error': ('异常', 'error'),
|
||||
}
|
||||
|
||||
text, tag = status_map.get(status, (status, 'pending'))
|
||||
|
||||
item_id = str(index)
|
||||
if self.exists(item_id):
|
||||
values = self.item(item_id, 'values')
|
||||
cookie_display = cookie[:30] + '...' if len(cookie) > 30 else cookie
|
||||
self.item(item_id, values=(
|
||||
values[0],
|
||||
values[1],
|
||||
values[2],
|
||||
text,
|
||||
cookie_display if cookie else '-'
|
||||
), tags=(tag,))
|
||||
|
||||
def clear(self):
|
||||
"""清空表格"""
|
||||
for item in self.get_children():
|
||||
self.delete(item)
|
||||
|
||||
def get_all_items(self):
|
||||
"""获取所有项目"""
|
||||
items = []
|
||||
for item_id in self.get_children():
|
||||
values = self.item(item_id, 'values')
|
||||
items.append({
|
||||
'index': values[0],
|
||||
'username': values[1],
|
||||
'email': values[2],
|
||||
'status': values[3],
|
||||
'cookie': values[4],
|
||||
})
|
||||
return items
|
||||
|
||||
|
||||
class LogPanel(tk.Text):
|
||||
"""日志输出面板"""
|
||||
|
||||
def __init__(self, parent, **kwargs):
|
||||
super().__init__(
|
||||
parent,
|
||||
wrap='word',
|
||||
state='disabled',
|
||||
font=('Consolas', 10),
|
||||
**kwargs
|
||||
)
|
||||
|
||||
# 日志级别颜色
|
||||
self.tag_configure('info', foreground='#333333')
|
||||
self.tag_configure('success', foreground='#008000')
|
||||
self.tag_configure('warning', foreground='#FF8C00')
|
||||
self.tag_configure('error', foreground='#FF0000')
|
||||
self.tag_configure('debug', foreground='#888888')
|
||||
self.tag_configure('timestamp', foreground='#666666')
|
||||
|
||||
# 添加滚动条
|
||||
scrollbar = ttk.Scrollbar(parent, orient='vertical', command=self.yview)
|
||||
self.configure(yscrollcommand=scrollbar.set)
|
||||
scrollbar.pack(side='right', fill='y')
|
||||
|
||||
def append_log(self, level: str, message: str):
|
||||
"""添加日志"""
|
||||
self.configure(state='normal')
|
||||
|
||||
# 添加时间戳
|
||||
timestamp = datetime.now().strftime('%H:%M:%S')
|
||||
self.insert('end', f'[{timestamp}] ', 'timestamp')
|
||||
|
||||
# 添加日志内容
|
||||
self.insert('end', f'{message}\n', level)
|
||||
|
||||
# 滚动到底部
|
||||
self.see('end')
|
||||
|
||||
self.configure(state='disabled')
|
||||
|
||||
def clear(self):
|
||||
"""清空日志"""
|
||||
self.configure(state='normal')
|
||||
self.delete('1.0', 'end')
|
||||
self.configure(state='disabled')
|
||||
|
||||
|
||||
class ImportDialog(tk.Toplevel):
|
||||
"""导入对话框"""
|
||||
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent)
|
||||
|
||||
self.title('从文件导入')
|
||||
self.geometry('400x150')
|
||||
self.resizable(False, False)
|
||||
self.transient(parent)
|
||||
self.grab_set()
|
||||
|
||||
# 居中显示
|
||||
self.update_idletasks()
|
||||
x = (self.winfo_screenwidth() - 400) // 2
|
||||
y = (self.winfo_screenheight() - 150) // 2
|
||||
self.geometry(f'+{x}+{y}')
|
||||
|
||||
self.filepath = None
|
||||
self.result = None
|
||||
|
||||
# 文件路径
|
||||
frame = ttk.Frame(self, padding=20)
|
||||
frame.pack(fill='both', expand=True)
|
||||
|
||||
ttk.Label(frame, text='选择账号文件(每行一个,格式:用户名|密码|邮箱|邮箱密码)').pack(anchor='w')
|
||||
|
||||
path_frame = ttk.Frame(frame)
|
||||
path_frame.pack(fill='x', pady=(10, 0))
|
||||
|
||||
self.path_var = tk.StringVar()
|
||||
ttk.Entry(path_frame, textvariable=self.path_var, state='readonly').pack(side='left', fill='x', expand=True)
|
||||
ttk.Button(path_frame, text='浏览', command=self._browse).pack(side='left', padx=(5, 0))
|
||||
|
||||
# 按钮
|
||||
btn_frame = ttk.Frame(frame)
|
||||
btn_frame.pack(fill='x', pady=(20, 0))
|
||||
|
||||
ttk.Button(btn_frame, text='确定', command=self._confirm).pack(side='right', padx=(5, 0))
|
||||
ttk.Button(btn_frame, text='取消', command=self._cancel).pack(side='right')
|
||||
|
||||
def _browse(self):
|
||||
"""浏览文件"""
|
||||
from tkinter import filedialog
|
||||
filepath = filedialog.askopenfilename(
|
||||
title='选择账号文件',
|
||||
filetypes=[('文本文件', '*.txt'), ('所有文件', '*.*')]
|
||||
)
|
||||
if filepath:
|
||||
self.path_var.set(filepath)
|
||||
self.filepath = filepath
|
||||
|
||||
def _confirm(self):
|
||||
"""确认"""
|
||||
if not self.filepath:
|
||||
tk.messagebox.showwarning('提示', '请选择文件')
|
||||
return
|
||||
self.result = self.filepath
|
||||
self.destroy()
|
||||
|
||||
def _cancel(self):
|
||||
"""取消"""
|
||||
self.destroy()
|
||||
@@ -1,165 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
斗鱼自动登录工具 - Python版
|
||||
|
||||
功能:
|
||||
1. 自动登录斗鱼账号
|
||||
2. 自动过极验滑块验证
|
||||
3. 自动获取邮箱验证码
|
||||
4. 批量获取Cookie
|
||||
"""
|
||||
|
||||
import sys
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from loguru import logger
|
||||
|
||||
from douyu import DouyuLogin, Config
|
||||
from utils import setup_logger, save_cookie, load_accounts
|
||||
|
||||
|
||||
def login_single(config: Config, account_index: int = 0) -> str:
|
||||
"""
|
||||
单账号登录
|
||||
|
||||
Args:
|
||||
config: 配置对象
|
||||
account_index: 账号索引
|
||||
|
||||
Returns:
|
||||
Cookie字符串
|
||||
"""
|
||||
accounts = config.get_accounts()
|
||||
|
||||
if account_index >= len(accounts):
|
||||
logger.error(f"账号索引 {account_index} 超出范围,共有 {len(accounts)} 个账号")
|
||||
return ""
|
||||
|
||||
account = accounts[account_index]
|
||||
logger.info(f"登录账号: {account.username}")
|
||||
|
||||
# 创建登录器
|
||||
proxy = config.get_proxy()
|
||||
proxy_url = None
|
||||
proxy_api_url = None
|
||||
|
||||
if proxy.enabled:
|
||||
if proxy.api_url:
|
||||
# 使用代理API
|
||||
proxy_api_url = proxy.api_url
|
||||
elif proxy.http or proxy.https:
|
||||
# 使用静态代理
|
||||
proxy_url = {
|
||||
'http': proxy.http or proxy.https,
|
||||
'https': proxy.https or proxy.http,
|
||||
}
|
||||
|
||||
# 获取极验配置
|
||||
geetest_config = config.get_geetest_config()
|
||||
max_retries = geetest_config.get('max_retries', 5)
|
||||
|
||||
loginer = DouyuLogin(
|
||||
account,
|
||||
proxy=proxy_url,
|
||||
proxy_api_url=proxy_api_url,
|
||||
max_geetest_retries=max_retries,
|
||||
)
|
||||
|
||||
# 执行登录
|
||||
result = loginer.login()
|
||||
|
||||
if result.success:
|
||||
# 保存Cookie
|
||||
cookie_dir = config.get_cookie_dir()
|
||||
save_cookie(account.username, result.cookie, cookie_dir)
|
||||
return result.cookie
|
||||
else:
|
||||
logger.error(f"登录失败: {result.message}")
|
||||
return ""
|
||||
|
||||
|
||||
def login_batch(config: Config) -> dict:
|
||||
"""
|
||||
批量登录
|
||||
|
||||
Returns:
|
||||
{username: cookie} 字典
|
||||
"""
|
||||
accounts = config.get_accounts()
|
||||
results = {}
|
||||
|
||||
logger.info(f"开始批量登录,共 {len(accounts)} 个账号")
|
||||
|
||||
for i, account in enumerate(accounts):
|
||||
logger.info(f"[{i+1}/{len(accounts)}] 登录账号: {account.username}")
|
||||
|
||||
try:
|
||||
cookie = login_single(config, i)
|
||||
if cookie:
|
||||
results[account.username] = cookie
|
||||
logger.success(f"账号 {account.username} 登录成功")
|
||||
else:
|
||||
logger.error(f"账号 {account.username} 登录失败")
|
||||
except Exception as e:
|
||||
logger.error(f"账号 {account.username} 登录异常: {e}")
|
||||
|
||||
# 统计结果
|
||||
success_count = len(results)
|
||||
total_count = len(accounts)
|
||||
logger.info(f"批量登录完成: 成功 {success_count}/{total_count}")
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
parser = argparse.ArgumentParser(description='斗鱼自动登录工具')
|
||||
parser.add_argument('-c', '--config', default='config.yaml', help='配置文件路径')
|
||||
parser.add_argument('-i', '--index', type=int, default=0, help='单账号登录时的账号索引')
|
||||
parser.add_argument('-b', '--batch', action='store_true', help='批量登录模式')
|
||||
parser.add_argument('-g', '--gui', action='store_true', help='启动GUI界面')
|
||||
parser.add_argument('-w', '--web', action='store_true', help='启动Web后台')
|
||||
parser.add_argument('-v', '--verbose', action='store_true', help='详细日志')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# 配置日志
|
||||
log_level = "DEBUG" if args.verbose else "INFO"
|
||||
setup_logger(level=log_level)
|
||||
|
||||
# 启动GUI
|
||||
if args.gui:
|
||||
from gui import DouyuLoginApp
|
||||
app = DouyuLoginApp()
|
||||
app.run()
|
||||
return
|
||||
|
||||
# 启动Web后台
|
||||
if args.web:
|
||||
from web.backend.main import run
|
||||
run()
|
||||
return
|
||||
|
||||
# 加载配置
|
||||
try:
|
||||
config = Config(args.config)
|
||||
except FileNotFoundError as e:
|
||||
logger.error(str(e))
|
||||
sys.exit(1)
|
||||
|
||||
# 执行登录
|
||||
if args.batch:
|
||||
results = login_batch(config)
|
||||
print(f"\n登录结果: 成功 {len(results)} 个账号")
|
||||
else:
|
||||
cookie = login_single(config, args.index)
|
||||
if cookie:
|
||||
print(f"\n登录成功!")
|
||||
print(f"Cookie: {cookie[:50]}...")
|
||||
else:
|
||||
print("\n登录失败")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
+3
-7
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "douyu-login-py"
|
||||
version = "0.1.0"
|
||||
description = "斗鱼自动登录工具 - Python版"
|
||||
version = "0.2.0"
|
||||
description = "斗鱼批量登录 Web 后台"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12,<3.13"
|
||||
dependencies = [
|
||||
@@ -10,7 +10,6 @@ dependencies = [
|
||||
"numpy>=1.24.0",
|
||||
"opencv-python-headless>=4.8.0",
|
||||
"Pillow>=10.0.0",
|
||||
"PyYAML>=6.0",
|
||||
"loguru>=0.7.0",
|
||||
"fastapi>=0.110.0",
|
||||
"uvicorn[standard]>=0.27.0",
|
||||
@@ -21,12 +20,9 @@ dependencies = [
|
||||
"python-multipart>=0.0.9",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
douyu-login = "main:main"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["douyu", "geetest", "utils", "gui", "web"]
|
||||
packages = ["core", "utils", "web"]
|
||||
|
||||
@@ -3,9 +3,7 @@ pycryptodome>=3.19.0
|
||||
numpy>=1.24.0
|
||||
opencv-python-headless>=4.8.0
|
||||
Pillow>=10.0.0
|
||||
PyYAML>=6.0
|
||||
loguru>=0.7.0
|
||||
# Web 后端
|
||||
fastapi>=0.110.0
|
||||
uvicorn[standard]>=0.27.0
|
||||
sqlalchemy>=2.0.0
|
||||
|
||||
@@ -1,202 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""邮箱IMAP读取测试脚本"""
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
# 添加项目根目录到path
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
from douyu.config import Config
|
||||
from douyu.email_verifier import EmailVerifier
|
||||
from utils.logger import setup_logger
|
||||
|
||||
|
||||
def mask_email(email_address: str) -> str:
|
||||
"""隐藏邮箱中间部分,避免控制台泄露完整账号。"""
|
||||
if "@" not in email_address:
|
||||
return email_address
|
||||
|
||||
name, domain = email_address.split("@", 1)
|
||||
if len(name) <= 2:
|
||||
masked_name = name[0] + "*"
|
||||
else:
|
||||
masked_name = name[:2] + "*" * max(2, len(name) - 4) + name[-2:]
|
||||
|
||||
return f"{masked_name}@{domain}"
|
||||
|
||||
|
||||
def clean_preview(text: str, max_length: int = 120) -> str:
|
||||
"""清理正文预览,保持输出紧凑。"""
|
||||
preview = " ".join(text.split())
|
||||
preview = re.sub(r"\b\d{6}\b", "******", preview)
|
||||
if len(preview) <= max_length:
|
||||
return preview
|
||||
return preview[:max_length] + "..."
|
||||
|
||||
|
||||
def format_code(code: str, show_code: bool) -> str:
|
||||
"""格式化验证码输出,默认隐藏大部分数字。"""
|
||||
if not code:
|
||||
return "未提取到"
|
||||
if show_code:
|
||||
return code
|
||||
return f"****{code[-2:]}"
|
||||
|
||||
|
||||
def format_message_time(verifier: EmailVerifier, msg) -> str:
|
||||
"""格式化邮件时间。"""
|
||||
message_time = verifier._parse_message_time(msg)
|
||||
if not message_time:
|
||||
return "未知"
|
||||
return message_time.strftime("%Y-%m-%d %H:%M:%S %z")
|
||||
|
||||
|
||||
def load_account(config_path: str, index: int):
|
||||
"""从配置文件加载指定账号。"""
|
||||
config = Config(config_path)
|
||||
accounts = config.get_accounts()
|
||||
if not accounts:
|
||||
raise ValueError("config.yaml 中没有配置账号")
|
||||
|
||||
if index < 0 or index >= len(accounts):
|
||||
raise IndexError(f"账号索引超出范围: {index},当前共有 {len(accounts)} 个账号")
|
||||
|
||||
return accounts[index]
|
||||
|
||||
|
||||
def list_recent_messages(
|
||||
verifier: EmailVerifier,
|
||||
lookback_minutes: int,
|
||||
limit: int,
|
||||
show_code: bool,
|
||||
) -> None:
|
||||
"""列出最近邮件,并测试斗鱼验证码解析。"""
|
||||
verifier.connect()
|
||||
|
||||
try:
|
||||
status, _ = verifier._connection.select(verifier.mailbox, readonly=True)
|
||||
if status != "OK":
|
||||
raise RuntimeError(f"选择邮箱目录失败: {verifier.mailbox}")
|
||||
|
||||
since_dt = datetime.now() - timedelta(minutes=lookback_minutes)
|
||||
since = verifier._format_imap_date(since_dt)
|
||||
status, messages = verifier._connection.search(None, "SINCE", since)
|
||||
if status != "OK":
|
||||
raise RuntimeError(f"搜索邮件失败: status={status}")
|
||||
|
||||
message_ids = messages[0].split() if messages and messages[0] else []
|
||||
if not message_ids:
|
||||
print(f"最近 {lookback_minutes} 分钟没有邮件")
|
||||
return
|
||||
|
||||
recent_ids = list(reversed(message_ids[-limit:]))
|
||||
print(f"搜索范围: 最近 {lookback_minutes} 分钟")
|
||||
print(f"匹配邮件数: {len(message_ids)},展示最新 {len(recent_ids)} 封\n")
|
||||
|
||||
for index, message_id in enumerate(recent_ids, start=1):
|
||||
msg = verifier._fetch_message(message_id)
|
||||
if not msg:
|
||||
print(f"[{index}] 读取失败: id={message_id.decode(errors='ignore')}")
|
||||
continue
|
||||
|
||||
subject = verifier._decode_subject(msg.get("Subject", ""))
|
||||
from_addr = msg.get("From", "")
|
||||
body = verifier._get_email_body(msg)
|
||||
is_douyu = verifier._is_douyu_email(subject, from_addr, body)
|
||||
code = verifier._extract_verification_code(body) if is_douyu else ""
|
||||
|
||||
print(f"[{index}] 邮件ID: {message_id.decode(errors='ignore')}")
|
||||
print(f" 时间: {format_message_time(verifier, msg)}")
|
||||
print(f" 发件人: {from_addr}")
|
||||
print(f" 主题: {subject}")
|
||||
print(f" 斗鱼邮件: {'是' if is_douyu else '否'}")
|
||||
print(f" 验证码: {format_code(code, show_code)}")
|
||||
print(f" 正文预览: {clean_preview(body)}\n")
|
||||
finally:
|
||||
verifier.disconnect()
|
||||
|
||||
|
||||
def wait_verification_code(
|
||||
account,
|
||||
max_wait: int,
|
||||
interval: int,
|
||||
new_only: bool,
|
||||
show_code: bool,
|
||||
) -> None:
|
||||
"""等待并提取斗鱼验证码,不负责发送验证码邮件。"""
|
||||
verifier = EmailVerifier(
|
||||
imap_server=account.email_imap_server,
|
||||
imap_port=account.email_imap_port,
|
||||
username=account.email,
|
||||
password=account.email_password,
|
||||
)
|
||||
|
||||
after_timestamp = time.time() if new_only else None
|
||||
code = verifier.get_verification_code(
|
||||
max_wait=max_wait,
|
||||
interval=interval,
|
||||
after_timestamp=after_timestamp,
|
||||
)
|
||||
print(f"等待验证码结果: {format_code(code, show_code)}")
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
"""解析命令行参数。"""
|
||||
parser = argparse.ArgumentParser(description="测试邮箱IMAP是否能读取邮件和验证码")
|
||||
parser.add_argument("-c", "--config", default="config.yaml", help="配置文件路径")
|
||||
parser.add_argument("-i", "--index", type=int, default=0, help="账号索引")
|
||||
parser.add_argument("--lookback-minutes", type=int, default=30, help="读取最近多少分钟邮件")
|
||||
parser.add_argument("--limit", type=int, default=5, help="展示最近多少封邮件")
|
||||
parser.add_argument("--wait-code", action="store_true", help="额外等待并提取斗鱼验证码")
|
||||
parser.add_argument("--new-only", action="store_true", help="等待验证码时只接受脚本启动后的新邮件")
|
||||
parser.add_argument("--show-code", action="store_true", help="输出完整验证码")
|
||||
parser.add_argument("--max-wait", type=int, default=60, help="等待验证码最大秒数")
|
||||
parser.add_argument("--interval", type=int, default=2, help="验证码轮询间隔秒数")
|
||||
parser.add_argument("--log-level", default="INFO", help="日志级别")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""脚本入口。"""
|
||||
args = parse_args()
|
||||
setup_logger(level=args.log_level)
|
||||
|
||||
account = load_account(args.config, args.index)
|
||||
print("=== 邮箱IMAP读取测试 ===")
|
||||
print(f"账号索引: {args.index}")
|
||||
print(f"邮箱账号: {mask_email(account.email)}")
|
||||
print(f"IMAP服务器: {account.email_imap_server}:{account.email_imap_port}\n")
|
||||
|
||||
verifier = EmailVerifier(
|
||||
imap_server=account.email_imap_server,
|
||||
imap_port=account.email_imap_port,
|
||||
username=account.email,
|
||||
password=account.email_password,
|
||||
lookback_minutes=args.lookback_minutes,
|
||||
max_messages=args.limit,
|
||||
)
|
||||
list_recent_messages(
|
||||
verifier=verifier,
|
||||
lookback_minutes=args.lookback_minutes,
|
||||
limit=args.limit,
|
||||
show_code=args.show_code,
|
||||
)
|
||||
|
||||
if args.wait_code:
|
||||
print("=== 等待斗鱼验证码 ===")
|
||||
wait_verification_code(
|
||||
account=account,
|
||||
max_wait=args.max_wait,
|
||||
interval=args.interval,
|
||||
new_only=args.new_only,
|
||||
show_code=args.show_code,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,98 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
测试登录功能
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# 添加项目根目录到path
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
from douyu.config import Config, Account
|
||||
from douyu.crypto import encrypt_password, encrypt_nickname_or_phone
|
||||
from douyu.email_verifier import EmailVerifier
|
||||
from utils.logger import setup_logger
|
||||
|
||||
|
||||
def test_crypto():
|
||||
"""测试加密功能"""
|
||||
print("=== 测试加密功能 ===")
|
||||
|
||||
# 测试密码加密
|
||||
password = "test_password"
|
||||
encrypted = encrypt_password(password)
|
||||
print(f"密码: {password}")
|
||||
print(f"MD5: {encrypted}")
|
||||
|
||||
# 测试用户名加密
|
||||
username = "test_user"
|
||||
encrypted = encrypt_nickname_or_phone(username)
|
||||
print(f"用户名: {username}")
|
||||
print(f"加密后: {encrypted}")
|
||||
|
||||
print()
|
||||
|
||||
|
||||
def test_config():
|
||||
"""测试配置加载"""
|
||||
print("=== 测试配置加载 ===")
|
||||
|
||||
try:
|
||||
config = Config("config.yaml")
|
||||
accounts = config.get_accounts()
|
||||
|
||||
print(f"加载了 {len(accounts)} 个账号")
|
||||
for i, acc in enumerate(accounts):
|
||||
print(f" [{i}] {acc.username} - {acc.email}")
|
||||
|
||||
proxy = config.get_proxy()
|
||||
print(f"代理: {'启用' if proxy.enabled else '禁用'}")
|
||||
|
||||
except FileNotFoundError as e:
|
||||
print(f"配置文件不存在: {e}")
|
||||
except Exception as e:
|
||||
print(f"加载配置失败: {e}")
|
||||
|
||||
print()
|
||||
|
||||
|
||||
def test_email_verifier():
|
||||
"""测试邮箱验证器(不实际连接)"""
|
||||
print("=== 测试邮箱验证器 ===")
|
||||
|
||||
# 测试验证码提取
|
||||
verifier = EmailVerifier("", "", "", "")
|
||||
|
||||
# 模拟邮件内容
|
||||
test_cases = [
|
||||
"您的验证码是:123456,请在5分钟内完成验证。",
|
||||
"验证码:654321",
|
||||
"Your verification code is 789012",
|
||||
"【斗鱼】安全验证码:345678,切勿泄露给他人!",
|
||||
]
|
||||
|
||||
for text in test_cases:
|
||||
code = verifier._extract_verification_code(text)
|
||||
print(f"文本: {text[:30]}...")
|
||||
print(f"验证码: {code}")
|
||||
print()
|
||||
|
||||
print()
|
||||
|
||||
|
||||
def main():
|
||||
"""运行所有测试"""
|
||||
setup_logger(level="INFO")
|
||||
|
||||
print("斗鱼自动登录工具 - 测试\n")
|
||||
|
||||
test_crypto()
|
||||
test_config()
|
||||
test_email_verifier()
|
||||
|
||||
print("测试完成!")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
+1
-2
@@ -1,6 +1,5 @@
|
||||
"""工具模块"""
|
||||
|
||||
from .logger import setup_logger
|
||||
from .helpers import load_accounts, save_cookie
|
||||
|
||||
__all__ = ["setup_logger", "load_accounts", "save_cookie"]
|
||||
__all__ = ["setup_logger"]
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
"""辅助工具函数"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
from loguru import logger
|
||||
|
||||
from douyu.config import Account
|
||||
|
||||
|
||||
def load_accounts(config_path: str = "config.yaml") -> List[Account]:
|
||||
"""加载账号列表"""
|
||||
from douyu.config import Config
|
||||
|
||||
config = Config(config_path)
|
||||
accounts = config.get_accounts()
|
||||
|
||||
logger.info(f"加载了 {len(accounts)} 个账号")
|
||||
return accounts
|
||||
|
||||
|
||||
def save_cookie(username: str, cookie: str, cookie_dir: str = "data/cookies") -> str:
|
||||
"""
|
||||
保存Cookie到文件
|
||||
|
||||
Args:
|
||||
username: 用户名
|
||||
cookie: Cookie字符串
|
||||
cookie_dir: Cookie存储目录
|
||||
|
||||
Returns:
|
||||
文件路径
|
||||
"""
|
||||
import time
|
||||
|
||||
cookie_path = Path(cookie_dir)
|
||||
cookie_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
filepath = cookie_path / f"{username}.json"
|
||||
|
||||
data = {
|
||||
'username': username,
|
||||
'cookie': cookie,
|
||||
'timestamp': int(time.time()),
|
||||
}
|
||||
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
logger.info(f"Cookie已保存: {filepath}")
|
||||
return str(filepath)
|
||||
|
||||
|
||||
def load_cookie(username: str, cookie_dir: str = "data/cookies") -> str:
|
||||
"""
|
||||
从文件加载Cookie
|
||||
|
||||
Args:
|
||||
username: 用户名
|
||||
cookie_dir: Cookie存储目录
|
||||
|
||||
Returns:
|
||||
Cookie字符串,不存在返回空字符串
|
||||
"""
|
||||
filepath = Path(cookie_dir) / f"{username}.json"
|
||||
|
||||
if not filepath.exists():
|
||||
return ""
|
||||
|
||||
try:
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
return data.get('cookie', '')
|
||||
except Exception as e:
|
||||
logger.error(f"加载Cookie失败: {e}")
|
||||
return ""
|
||||
|
||||
|
||||
def format_cookie_for_browser(cookie_str: str) -> dict:
|
||||
"""
|
||||
将Cookie字符串转换为浏览器格式
|
||||
|
||||
Args:
|
||||
cookie_str: Cookie字符串
|
||||
|
||||
Returns:
|
||||
Cookie字典
|
||||
"""
|
||||
cookies = {}
|
||||
for item in cookie_str.split(';'):
|
||||
item = item.strip()
|
||||
if '=' in item:
|
||||
key, value = item.split('=', 1)
|
||||
cookies[key.strip()] = value.strip()
|
||||
return cookies
|
||||
Binary file not shown.
Binary file not shown.
@@ -70,7 +70,7 @@ def import_accounts(
|
||||
current: User = Depends(require_permission("account:import")),
|
||||
):
|
||||
"""批量导入账号。格式:用户名|密码|邮箱|邮箱密码"""
|
||||
from douyu.email_verifier import get_email_config_for_account
|
||||
from core.douyu.email_verifier import get_email_config_for_account
|
||||
|
||||
accounts = []
|
||||
skipped = 0
|
||||
|
||||
@@ -97,7 +97,7 @@ def test_whitelist(
|
||||
current: User = Depends(require_permission("whitelist:test")),
|
||||
):
|
||||
"""测试白名单连接并自动同步出口IP。"""
|
||||
from douyu.whitelist import WhitelistManager, get_exit_ip_via_proxy
|
||||
from core.douyu.whitelist import WhitelistManager, get_exit_ip_via_proxy
|
||||
import requests as req_lib
|
||||
|
||||
cfg = _get_or_create(db)
|
||||
|
||||
Binary file not shown.
@@ -9,8 +9,8 @@ from typing import Optional
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from douyu import DouyuLogin
|
||||
from douyu.config import Account, ProxyConfig as DouyuProxyConfig
|
||||
from core.douyu import DouyuLogin
|
||||
from core.models import Account, ProxyConfig as DouyuProxyConfig
|
||||
from ..models import Account as AccountModel, LoginTask, ProxyConfig as ProxyConfigModel
|
||||
from ..permissions import has_permission
|
||||
|
||||
|
||||
Reference in New Issue
Block a user