532 lines
19 KiB
Python
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()
|