初步增加 gui
This commit is contained in:
@@ -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",
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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()
|
||||
@@ -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
@@ -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()
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user