diff --git a/ck对比.md b/ck对比.md new file mode 100644 index 0000000..38e4ab4 --- /dev/null +++ b/ck对比.md @@ -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", + + + + diff --git a/gui/__init__.py b/gui/__init__.py new file mode 100644 index 0000000..983586f --- /dev/null +++ b/gui/__init__.py @@ -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', +] diff --git a/gui/app.py b/gui/app.py new file mode 100644 index 0000000..f8629d7 --- /dev/null +++ b/gui/app.py @@ -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() diff --git a/gui/login_worker.py b/gui/login_worker.py new file mode 100644 index 0000000..c0be273 --- /dev/null +++ b/gui/login_worker.py @@ -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, + }) diff --git a/gui/widgets.py b/gui/widgets.py new file mode 100644 index 0000000..000649f --- /dev/null +++ b/gui/widgets.py @@ -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() diff --git a/main.py b/main.py index dbda755..3d62263 100644 --- a/main.py +++ b/main.py @@ -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)