Files
live-hub-py/gui/app.py
T
2026-06-21 23:25:08 +08:00

532 lines
19 KiB
Python

"""主窗口模块"""
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()