初步增加 gui

This commit is contained in:
yml2213
2026-06-21 23:25:08 +08:00
parent ea8a56ca66
commit af3951ec6f
6 changed files with 938 additions and 0 deletions
+14
View File
@@ -0,0 +1,14 @@
软软马卡龙(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",
+13
View File
@@ -0,0 +1,13 @@
"""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',
]
+531
View File
@@ -0,0 +1,531 @@
"""主窗口模块"""
import json
import threading
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
from datetime import datetime
from pathlib import Path
from queue import Queue, Empty
from typing import List
from douyu.config import Account, ProxyConfig
from .login_worker import LoginWorker, parse_accounts_text
from .widgets import AccountTable, LogPanel
class DouyuLoginApp:
"""斗鱼批量登录GUI应用"""
def __init__(self, config_path: str = 'config.yaml'):
self.config_path = config_path
self.accounts: List[Account] = []
self.workers: List[LoginWorker] = []
self.results: List[dict] = []
self.is_running = False
# 队列用于线程间通信
self.result_queue = Queue()
self.log_queue = Queue()
# 加载配置
self.proxy_config = self._load_proxy_config()
self.geetest_retries = self._load_geetest_retries()
# 创建主窗口
self.root = tk.Tk()
self.root.title('斗鱼批量登录工具')
self.root.geometry('900x700')
self.root.minsize(800, 600)
# 设置图标(如果存在)
try:
self.root.iconbitmap('icon.ico')
except:
pass
# 创建界面
self._create_ui()
# 定时检查队列
self._poll_queues()
def _load_proxy_config(self) -> ProxyConfig:
"""从配置文件加载代理配置"""
try:
import yaml
with open(self.config_path, 'r', encoding='utf-8') as f:
config = yaml.safe_load(f)
proxy = config.get('proxy', {})
return ProxyConfig(
enabled=proxy.get('enabled', False),
api_url=proxy.get('api_url', ''),
http=proxy.get('http', ''),
https=proxy.get('https', ''),
)
except:
return ProxyConfig()
def _load_geetest_retries(self) -> int:
"""从配置文件加载极验重试次数"""
try:
import yaml
with open(self.config_path, 'r', encoding='utf-8') as f:
config = yaml.safe_load(f)
geetest = config.get('geetest', {})
return geetest.get('max_retries', 5)
except:
return 5
def _create_ui(self):
"""创建用户界面"""
# 主容器
main_frame = ttk.Frame(self.root, padding=10)
main_frame.pack(fill='both', expand=True)
# 顶部:导入区域
self._create_import_section(main_frame)
# 配置区域:代理和极验
self._create_config_section(main_frame)
# 中部:账号列表
self._create_table_section(main_frame)
# 底部:按钮和日志
self._create_bottom_section(main_frame)
def _create_import_section(self, parent):
"""创建导入区域"""
frame = ttk.LabelFrame(parent, text='批量导入(每行一个,格式:用户名|密码|邮箱|邮箱密码)', padding=10)
frame.pack(fill='x', pady=(0, 10))
# 文本输入框
self.import_text = tk.Text(frame, height=5, font=('Consolas', 10))
self.import_text.pack(fill='x')
# 按钮栏
btn_frame = ttk.Frame(frame)
btn_frame.pack(fill='x', pady=(5, 0))
ttk.Button(btn_frame, text='解析导入', command=self._parse_import).pack(side='left')
ttk.Button(btn_frame, text='从文件导入', command=self._import_from_file).pack(side='left', padx=(5, 0))
ttk.Button(btn_frame, text='清空', command=self._clear_import).pack(side='left', padx=(5, 0))
# 提示标签
ttk.Label(btn_frame, text='示例:用户9006787894|a778899|jtmjcm@bdhg.xyz|www123',
foreground='gray').pack(side='right')
def _create_config_section(self, parent):
"""创建配置区域(代理和极验)"""
config_frame = ttk.Frame(parent)
config_frame.pack(fill='x', pady=(0, 10))
# 代理配置
proxy_frame = ttk.LabelFrame(config_frame, text='代理配置', padding=10)
proxy_frame.pack(side='left', fill='both', expand=True, padx=(0, 5))
# 启用代理
self.proxy_enabled_var = tk.BooleanVar(value=self.proxy_config.enabled)
ttk.Checkbutton(proxy_frame, text='启用代理', variable=self.proxy_enabled_var,
command=self._on_proxy_toggle).pack(anchor='w')
# 代理类型选择
type_frame = ttk.Frame(proxy_frame)
type_frame.pack(fill='x', pady=(5, 0))
self.proxy_type_var = tk.StringVar(value='api' if self.proxy_config.api_url else 'static')
ttk.Radiobutton(type_frame, text='API获取', variable=self.proxy_type_var,
value='api', command=self._on_proxy_type_change).pack(side='left')
ttk.Radiobutton(type_frame, text='静态代理', variable=self.proxy_type_var,
value='static', command=self._on_proxy_type_change).pack(side='left', padx=(10, 0))
# API地址
api_frame = ttk.Frame(proxy_frame)
api_frame.pack(fill='x', pady=(5, 0))
ttk.Label(api_frame, text='API地址:').pack(side='left')
self.proxy_api_var = tk.StringVar(value=self.proxy_config.api_url)
self.proxy_api_entry = ttk.Entry(api_frame, textvariable=self.proxy_api_var, width=50)
self.proxy_api_entry.pack(side='left', fill='x', expand=True, padx=(5, 0))
# 静态代理
static_frame = ttk.Frame(proxy_frame)
static_frame.pack(fill='x', pady=(5, 0))
ttk.Label(static_frame, text='HTTP:').pack(side='left')
self.proxy_http_var = tk.StringVar(value=self.proxy_config.http)
self.proxy_http_entry = ttk.Entry(static_frame, textvariable=self.proxy_http_var, width=20)
self.proxy_http_entry.pack(side='left', padx=(5, 10))
ttk.Label(static_frame, text='HTTPS:').pack(side='left')
self.proxy_https_var = tk.StringVar(value=self.proxy_config.https)
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))
# 极验配置
geetest_frame = ttk.LabelFrame(config_frame, text='极验配置', padding=10)
geetest_frame.pack(side='right', fill='y', padx=(5, 0))
ttk.Label(geetest_frame, text='最大重试:').pack(anchor='w')
self.geetest_retries_var = tk.IntVar(value=self.geetest_retries)
retries_spin = ttk.Spinbox(geetest_frame, from_=1, to=20, width=5,
textvariable=self.geetest_retries_var)
retries_spin.pack(anchor='w', pady=(5, 0))
# 初始状态
self._on_proxy_toggle()
def _create_table_section(self, parent):
"""创建表格区域"""
frame = ttk.LabelFrame(parent, text='账号列表', padding=10)
frame.pack(fill='both', expand=True, pady=(0, 10))
# 表格
self.table = AccountTable(frame)
self.table.pack(fill='both', expand=True)
def _create_bottom_section(self, parent):
"""创建底部区域"""
# 按钮栏
btn_frame = ttk.Frame(parent)
btn_frame.pack(fill='x', pady=(0, 10))
self.start_btn = ttk.Button(btn_frame, text='开始批量登录', command=self._start_login)
self.start_btn.pack(side='left')
self.stop_btn = ttk.Button(btn_frame, text='停止', command=self._stop_login, state='disabled')
self.stop_btn.pack(side='left', padx=(5, 0))
ttk.Button(btn_frame, text='导出结果', command=self._export_results).pack(side='left', padx=(5, 0))
ttk.Button(btn_frame, text='清空列表', command=self._clear_table).pack(side='left', padx=(5, 0))
# 进度标签
self.progress_var = tk.StringVar(value='就绪')
ttk.Label(btn_frame, textvariable=self.progress_var).pack(side='right')
# 日志区域
log_frame = ttk.LabelFrame(parent, text='日志输出', padding=10)
log_frame.pack(fill='both', expand=True)
self.log_panel = LogPanel(log_frame)
self.log_panel.pack(fill='both', expand=True)
def _on_proxy_toggle(self):
"""代理启用/禁用切换"""
enabled = self.proxy_enabled_var.get()
state = 'normal' if enabled else 'disabled'
self.proxy_api_entry.configure(state=state)
self.proxy_http_entry.configure(state=state)
self.proxy_https_entry.configure(state=state)
def _on_proxy_type_change(self):
"""代理类型切换"""
proxy_type = self.proxy_type_var.get()
if proxy_type == 'api':
self.proxy_api_entry.configure(state='normal')
self.proxy_http_entry.configure(state='disabled')
self.proxy_https_entry.configure(state='disabled')
else:
self.proxy_api_entry.configure(state='disabled')
self.proxy_http_entry.configure(state='normal')
self.proxy_https_entry.configure(state='normal')
def _get_proxy_config(self) -> ProxyConfig:
"""获取当前代理配置"""
enabled = self.proxy_enabled_var.get()
proxy_type = self.proxy_type_var.get()
if proxy_type == 'api':
return ProxyConfig(
enabled=enabled,
api_url=self.proxy_api_var.get().strip(),
http='',
https='',
)
else:
return ProxyConfig(
enabled=enabled,
api_url='',
http=self.proxy_http_var.get().strip(),
https=self.proxy_https_var.get().strip(),
)
def _parse_import(self):
"""解析导入的文本"""
text = self.import_text.get('1.0', 'end')
if not text.strip():
messagebox.showwarning('提示', '请输入账号数据')
return
accounts = parse_accounts_text(text)
if not accounts:
messagebox.showwarning('提示', '未解析到有效账号')
return
# 添加到列表
for acc in accounts:
self.accounts.append(acc)
self.table.add_account(len(self.accounts) - 1, acc.username, acc.email)
self.log_panel.append_log('info', f'成功导入 {len(accounts)} 个账号')
self.progress_var.set(f'{len(self.accounts)} 个账号')
# 清空输入框
self.import_text.delete('1.0', 'end')
def _import_from_file(self):
"""从文件导入"""
filepath = filedialog.askopenfilename(
title='选择账号文件',
filetypes=[('文本文件', '*.txt'), ('CSV文件', '*.csv'), ('所有文件', '*.*')]
)
if not filepath:
return
try:
with open(filepath, 'r', encoding='utf-8') as f:
text = f.read()
accounts = parse_accounts_text(text)
if not accounts:
messagebox.showwarning('提示', '文件中未解析到有效账号')
return
for acc in accounts:
self.accounts.append(acc)
self.table.add_account(len(self.accounts) - 1, acc.username, acc.email)
self.log_panel.append_log('info', f'从文件导入 {len(accounts)} 个账号')
self.progress_var.set(f'{len(self.accounts)} 个账号')
except Exception as e:
messagebox.showerror('错误', f'读取文件失败: {e}')
def _clear_import(self):
"""清空导入框"""
self.import_text.delete('1.0', 'end')
def _clear_table(self):
"""清空表格"""
if self.is_running:
messagebox.showwarning('提示', '登录进行中,无法清空')
return
self.accounts.clear()
self.results.clear()
self.table.clear()
self.progress_var.set('就绪')
self.log_panel.append_log('info', '已清空账号列表')
def _start_login(self):
"""开始批量登录"""
if not self.accounts:
messagebox.showwarning('提示', '请先导入账号')
return
if self.is_running:
return
# 获取当前GUI配置
proxy_config = self._get_proxy_config()
geetest_retries = self.geetest_retries_var.get()
# 验证代理配置
if proxy_config.enabled:
if proxy_config.api_url and not proxy_config.api_url.startswith('http'):
messagebox.showwarning('提示', '代理API地址格式不正确,需要以http://或https://开头')
return
if not proxy_config.api_url and not proxy_config.http and not proxy_config.https:
messagebox.showwarning('提示', '请填写代理地址')
return
self.is_running = True
self.results.clear()
self.workers.clear()
self.start_btn.configure(state='disabled')
self.stop_btn.configure(state='normal')
# 重置表格状态
for i in range(len(self.accounts)):
self.table.update_status(i, 'pending')
self.log_panel.append_log('info', f'开始批量登录,共 {len(self.accounts)} 个账号')
# 显示代理配置
if proxy_config.enabled:
if proxy_config.api_url:
self.log_panel.append_log('info', f'使用代理API: {proxy_config.api_url[:50]}...')
else:
proxy_url = proxy_config.http or proxy_config.https
self.log_panel.append_log('info', f'使用静态代理: {proxy_url}')
else:
self.log_panel.append_log('info', '未使用代理')
self.log_panel.append_log('info', f'极验最大重试: {geetest_retries}')
self.progress_var.set('登录中...')
# 启动登录线程
for i, account in enumerate(self.accounts):
worker = LoginWorker(
account=account,
index=i,
result_queue=self.result_queue,
log_queue=self.log_queue,
proxy_config=proxy_config,
max_geetest_retries=geetest_retries,
)
self.workers.append(worker)
self.table.update_status(i, 'running')
worker.start()
def _stop_login(self):
"""停止登录"""
if not self.is_running:
return
self.is_running = False
# 停止所有工作线程
for worker in self.workers:
worker.stop()
self.start_btn.configure(state='normal')
self.stop_btn.configure(state='disabled')
self.log_panel.append_log('warning', '已停止登录')
self.progress_var.set('已停止')
def _poll_queues(self):
"""定时检查队列"""
# 处理日志队列
try:
while True:
level, message = self.log_queue.get_nowait()
self.log_panel.append_log(level, message)
except Empty:
pass
# 处理结果队列
try:
while True:
result = self.result_queue.get_nowait()
self.results.append(result)
self.table.update_status(
result['index'],
result['status'],
result['cookie']
)
# 更新进度
completed = len(self.results)
total = len(self.accounts)
self.progress_var.set(f'完成 {completed}/{total}')
# 检查是否全部完成
if completed >= total:
self._on_login_complete()
except Empty:
pass
# 继续轮询
self.root.after(100, self._poll_queues)
def _on_login_complete(self):
"""登录完成回调"""
self.is_running = False
self.start_btn.configure(state='normal')
self.stop_btn.configure(state='disabled')
# 统计结果
success = sum(1 for r in self.results if r['status'] == 'success')
failed = len(self.results) - success
self.log_panel.append_log('info', f'批量登录完成: 成功 {success}, 失败 {failed}')
self.progress_var.set(f'完成: 成功 {success}, 失败 {failed}')
messagebox.showinfo('完成', f'批量登录完成\n成功: {success}\n失败: {failed}')
def _export_results(self):
"""导出结果"""
if not self.results:
messagebox.showwarning('提示', '没有可导出的结果')
return
# 选择保存路径
filepath = filedialog.asksaveasfilename(
title='导出结果',
defaultextension='.json',
filetypes=[
('JSON文件', '*.json'),
('CSV文件', '*.csv'),
('文本文件', '*.txt'),
],
initialfile=f'douyu_cookies_{datetime.now().strftime("%Y%m%d_%H%M%S")}'
)
if not filepath:
return
try:
if filepath.endswith('.json'):
self._export_json(filepath)
elif filepath.endswith('.csv'):
self._export_csv(filepath)
else:
self._export_txt(filepath)
self.log_panel.append_log('success', f'结果已导出: {filepath}')
messagebox.showinfo('成功', f'结果已导出到:\n{filepath}')
except Exception as e:
messagebox.showerror('错误', f'导出失败: {e}')
def _export_json(self, filepath: str):
"""导出为JSON"""
data = {
'export_time': datetime.now().isoformat(),
'total': len(self.results),
'success': sum(1 for r in self.results if r['status'] == 'success'),
'results': self.results,
}
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
def _export_csv(self, filepath: str):
"""导出为CSV"""
import csv
with open(filepath, 'w', encoding='utf-8-sig', newline='') as f:
writer = csv.writer(f)
writer.writerow(['序号', '用户名', '邮箱', '状态', 'Cookie', '消息'])
for r in self.results:
writer.writerow([
r['index'] + 1,
r['username'],
r['email'],
r['status'],
r['cookie'],
r['message'],
])
def _export_txt(self, filepath: str):
"""导出为TXT"""
with open(filepath, 'w', encoding='utf-8') as f:
f.write(f'# 斗鱼批量登录结果\n')
f.write(f'# 导出时间: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}\n')
f.write(f'# 格式: 用户名|密码|邮箱|邮箱密码|Cookie\n\n')
for r in self.results:
if r['status'] == 'success':
# 找到对应的账号信息
acc = self.accounts[r['index']]
f.write(f'{acc.username}|{acc.password}|{acc.email}|{acc.email_password}|{r["cookie"]}\n')
def run(self):
"""运行应用"""
self.root.mainloop()
+160
View File
@@ -0,0 +1,160 @@
"""登录工作线程模块"""
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
# 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',
}
def get_imap_server(email: str) -> str:
"""从邮箱地址推导IMAP服务器"""
domain = email.split('@')[-1].lower()
return IMAP_SERVERS.get(domain, f'imap.{domain}')
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 = line.split('|')
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
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.api_url:
proxy_api_url = self.proxy_config.api_url
elif self.proxy_config.http or self.proxy_config.https:
proxy_url = {
'http': self.proxy_config.http or self.proxy_config.https,
'https': self.proxy_config.https or self.proxy_config.http,
}
# 创建登录器
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,
})
+212
View File
@@ -0,0 +1,212 @@
"""自定义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()
+8
View File
@@ -117,6 +117,7 @@ def main():
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('-v', '--verbose', action='store_true', help='详细日志')
args = parser.parse_args()
@@ -125,6 +126,13 @@ def main():
log_level = "DEBUG" if args.verbose else "INFO"
setup_logger(level=log_level)
# 启动GUI
if args.gui:
from gui import DouyuLoginApp
app = DouyuLoginApp(config_path=args.config)
app.run()
return
# 加载配置
try:
config = Config(args.config)