M3U8通用下载器
本项目是一款基于Python开发的GUI应用程序,专门用于下载和处理M3U8流媒体视频。该工具集成了现代化的用户界面、多线程下载、AES加密解密、文件合并等核心功能,为视频下载需求提供了完整的解决方案。
代码采用多线程以及队列用以优化程序性能,支持下载暂停、继续和停止操作;同时支持断点续传能力;对于下载好的ts片段,可使用传统的二进制合并方式,也可以使用第三方工具进行视频的合并(如FFmpeg等)
代码的某些功能可能会存在一些Bug,如果要完全使用,请自行修改。
代码如下:
"""
#!/usr/bin/env python3
# --*-- coding:UTF-8 --*--
@Author : LuoQiu
@Project : pip-view
@Software : PyCharm
@File : m3u8_GUI.py
@Time : 2025/09/09 12:06:30
"""
import os
import queue
import re
import tkinter as tk
import tkinter.font as tkfont
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime
from threading import Thread
from tkinter import filedialog, messagebox
import requests
import threading
import ttkbootstrap as ttk
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
from ttkbootstrap.constants import *class Spider(object):def __init__(self):# 设计软件界面self.root = ttk.Window(title="🎬 洛秋M3U8下载工具",themename='darkly', # 现代化深色主题size=(1400, 900),position=(300, 100),resizable=(True, True),alpha=0.98)# 设置窗口图标和样式self.setup_window_style()# 加载软件界面self._init_ui()# 加载菜单栏self.create_menu_bar()# 创建任务队列----表格数据更新self.task_queue = queue.Queue()# 创建线程锁-----表格数据更新--线程锁self.task_lock = threading.Lock()# 下载控制相关变量self.is_downloading = False # 是否正在下载self.is_paused = False # 是否暂停self.download_stopped = False # 是否停止下载self.executor = None # 线程池执行器self.task_url_queue = queue.Queue() # 下载任务队列self.download_path = None # 当前下载路径self.completion_dialog_shown = False # 防止重复弹出完成对话框# 每500ms监控队列self.root.after(500, self.process_task_queue)# 微信收款码Base64字符串self.wechat_img_str = """"""# 支付宝收款码Base64字符串self.alipay_img_str = """"""# 创建任务队列:下载地址self.task_url_queue = queue.Queue()# 定义菜单栏def create_menu_bar(self):"""创建菜单栏:return:"""menubar = tk.Menu(self.root)# 文件菜单file_menu = tk.Menu(menubar, tearoff=0)file_menu.add_command(label="打赏作者", command=self.parse_statement_text)file_menu.add_command(label="联系作者", command=self.parse_show_about)file_menu.add_separator()file_menu.add_command(label="退出", command=self.root.quit)menubar.add_cascade(label="打赏与联系作者", menu=file_menu)# 更新菜单update_menu = tk.Menu(menubar, tearoff=0)update_menu.add_command(label="检查更新", command=self.parse_show_update)menubar.add_cascade(label="更新", menu=update_menu)# 绑定菜单栏self.root.config(menu=menubar)# 功能函数:打赏作者def parse_statement_text(self):""":return:"""# 清除当前所有控件for widget in self.root.winfo_children():widget.destroy()# 创建充值页面主框架frame1 = ttk.Frame(self.root)frame1.grid(row=0, column=0, sticky=(N, W, E, S), padx=0, pady=0)# 标题title_label = ttk.Label(frame1, text="打赏作者", font=('微软雅黑', 22, 'bold'))title_label.pack(pady=(0, 10))# 配置根窗口的网格权重self.root.columnconfigure(0, weight=1)self.root.rowconfigure(1, weight=1)frame2 = ttk.Frame(self.root)frame2.grid(row=1, column=0, sticky=(N, W, E, S), padx=20, pady=20)frame2.columnconfigure(0, weight=1)frame2.rowconfigure(0, weight=1)# 创建居中的图片容器image_container = ttk.Frame(frame2)image_container.pack(expand=True, fill='both')# 创建一个内部框架来包含两个支付二维码,使其水平居中inner_frame = ttk.Frame(image_container)inner_frame.pack(expand=True, anchor='n', pady=(50, 0))# 微信二维码框架wechat_frame = ttk.Frame(inner_frame)wechat_frame.pack(side=tk.LEFT, padx=(0, 30))# 微信标题wechat_label = ttk.Label(wechat_frame, text="微信支付", font=('微软雅黑', 16, 'bold'))wechat_label.pack(pady=(0, 10))# 支付宝二维码框架alipay_frame = ttk.Frame(inner_frame)alipay_frame.pack(side=tk.LEFT)# 支付宝标题alipay_label = ttk.Label(alipay_frame, text="支付宝支付", font=('微软雅黑', 16, 'bold'))alipay_label.pack(pady=(0, 10))# 从base64字符串加载二维码图片from PIL import Image, ImageTkimport base64from io import BytesIOtry:# 微信二维码wechat_base64 = self.wechat_img_str # 假设微信二维码base64存储在wechat_img_str属性中wechat_base64 = wechat_base64.strip().replace('\n', '')wechat_img_data = base64.b64decode(wechat_base64)wechat_img = Image.open(BytesIO(wechat_img_data))wechat_img = wechat_img.resize((250, 250), Image.LANCZOS)wechat_photo = ImageTk.PhotoImage(wechat_img)wechat_image_label = ttk.Label(wechat_frame, image=wechat_photo)wechat_image_label.image = wechat_photowechat_image_label.pack()except Exception as e:ttk.Label(wechat_frame,text=f"微信二维码加载失败:\n{str(e)}",font=('微软雅黑', 10),padding=20,borderwidth=2,relief=tk.SOLID,background="white").pack()try:# 支付宝二维码alipay_base64 = self.alipay_img_str # 假设支付宝二维码base64存储在alipay_img_str属性中alipay_base64 = alipay_base64.strip().replace('\n', '')alipay_img_data = base64.b64decode(alipay_base64)alipay_img = Image.open(BytesIO(alipay_img_data))alipay_img = alipay_img.resize((250, 250), Image.LANCZOS)alipay_photo = ImageTk.PhotoImage(alipay_img)alipay_image_label = ttk.Label(alipay_frame, image=alipay_photo)alipay_image_label.image = alipay_photoalipay_image_label.pack()except Exception as e:ttk.Label(alipay_frame,text=f"支付宝二维码加载失败:\n{str(e)}",font=('微软雅黑', 10),padding=20,borderwidth=2,relief=tk.SOLID,background="white").pack()frame3 = ttk.Frame(self.root)frame3.grid(row=2, column=0, sticky=(N, W, E, S), padx=0, pady=0)# 按钮区域button_frame = ttk.Frame(frame3)button_frame.pack(fill=tk.X, pady=30, padx=50)# 退出按钮exit_btn = ttk.Button(button_frame,text="返回主界面",command=self.on_agreement_confirm,width=15,bootstyle="success")exit_btn.pack(side=tk.BOTTOM)# 功能函数:联系作者def parse_show_about(self):"""联系作者:return:"""# 清除当前所有控件for widget in self.root.winfo_children():widget.destroy()# 创建关于主框架,占满整个窗口main_frame = ttk.Frame(self.root, padding=20)main_frame.pack(fill=tk.BOTH, expand=True)# 标题title_label = ttk.Label(main_frame,text="洛秋m3u8下载工具 版本V1.0.0",font=('微软雅黑', 16, 'bold'))title_label.pack(pady=(0, 0))# 关于内容text_frame = ttk.Frame(main_frame)text_frame.pack(fill=tk.BOTH, expand=True)disclaimer_text = """洛秋m3u8下载工具 版本V1.0.0作者:洛秋"""text = tk.Text(text_frame,wrap=tk.WORD,font=('微软雅黑', 15),padx=0,pady=0,bg='#FFF9E6', # 浅黄色背景selectbackground='#FF9966',height=8)text.insert(tk.END, disclaimer_text)text.tag_add('title', '1.0', '2.0')text.tag_config('title', foreground='red', font=('微软雅黑', 15, 'bold'))text.config(state=tk.DISABLED)scrollbar = ttk.Scrollbar(text_frame, orient=tk.VERTICAL, command=text.yview)text.configure(yscrollcommand=scrollbar.set)scrollbar.pack(side=tk.RIGHT, fill=tk.Y)text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)# 确认机制confirm_frame = ttk.Frame(main_frame)confirm_frame.pack(fill=tk.X, pady=(0, 0))# 退出此界面btn_agree = ttk.Button(confirm_frame,text="返回主界面",state=tk.NORMAL,command=self.on_agreement_confirm,width=10,bootstyle="success")btn_agree.pack(side=tk.BOTTOM)# 功能函数:更新软件def parse_show_update(self):"""更新软件:return:"""# 清除当前所有控件for widget in self.root.winfo_children():widget.destroy()# 创建关于主框架,占满整个窗口main_frame = ttk.Frame(self.root, padding=20)main_frame.pack(fill=tk.BOTH, expand=True)# 标题title_label = ttk.Label(main_frame,text="洛秋m3u8下载工具 版本V1.0.0",font=('微软雅黑', 16, 'bold'))title_label.pack(pady=(0, 0))# 关于内容text_frame = ttk.Frame(main_frame)text_frame.pack(fill=tk.BOTH, expand=True)disclaimer_text = """洛秋m3u8下载工具 版本V1.0.0\r\n作者:洛秋\r\n浏览器访问以下链接,下载最新版本\r\nhttps://example.com"""text = tk.Text(text_frame,wrap=tk.WORD,font=('微软雅黑', 15),padx=0,pady=0,bg='#FFF9E6', # 浅黄色背景selectbackground='#FF9966',height=8)text.insert(tk.END, disclaimer_text)text.tag_add('title', '1.0', '2.0')text.tag_config('title', foreground='red', font=('微软雅黑', 15, 'bold'))text.config(state=tk.DISABLED)text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)# 确认机制confirm_frame = ttk.Frame(main_frame)confirm_frame.pack(fill=tk.X, pady=(0, 0))# 退出此界面btn_agree = ttk.Button(confirm_frame,text="返回主界面",state=tk.NORMAL,command=self.on_agreement_confirm,width=10,bootstyle="success")btn_agree.pack(side=tk.BOTTOM)# 用户确认同意法律声明后的回调函数def on_agreement_confirm(self):"""用户确认同意法律声明后的回调函数"""# 清除法律声明界面for widget in self.root.winfo_children():widget.destroy()# 重新加载菜单栏self.create_menu_bar()# 初始化主界面UIself._init_ui()# 设置窗口样式def setup_window_style(self):"""设置现代化窗口样式"""# 设置窗口最小尺寸self.root.minsize(1200, 800)# 配置网格权重,使界面可以自适应调整self.root.columnconfigure(0, weight=1)self.root.rowconfigure(0, weight=1)# 设置现代化字体和样式self.set_modern_font_style()# 加载现代化字体配置def set_modern_font_style(self):"""设置现代化字体样式"""# 创建多种字体大小,提升可读性self.large_title_font = tkfont.Font(family="Microsoft YaHei UI", size=10, weight="bold")self.title_font = tkfont.Font(family="Microsoft YaHei UI", size=9, weight="bold")self.normal_font = tkfont.Font(family="Microsoft YaHei UI", size=9)self.small_font = tkfont.Font(family="Microsoft YaHei UI", size=10)self.tiny_font = tkfont.Font(family="Microsoft YaHei UI", size=7)# 获取当前样式style = ttk.Style()# 配置现代化按钮样式style.configure("Modern.TButton",font=self.normal_font,padding=(18, 10),relief="flat",borderwidth=0)# 配置大按钮样式style.configure("Large.TButton",font=self.title_font,padding=(20, 12),relief="flat",borderwidth=0)# 配置标签样式style.configure("Title.TLabel",font=self.title_font,foreground="#ffffff")style.configure("Large.TLabel",font=self.large_title_font,foreground="#ffffff")style.configure("TLabel",font=self.normal_font)style.configure("Small.TLabel",font=self.small_font,foreground="#cccccc")# 配置输入框样式style.configure("Modern.TEntry",font=self.normal_font,fieldbackground="#2b2b2b",borderwidth=1,relief="solid",insertcolor="#ffffff",selectbackground="#0078d4")# 配置表格样式style.configure("Modern.Treeview",font=self.normal_font,rowheight=38,background="#2b2b2b",foreground="#ffffff",fieldbackground="#2b2b2b",borderwidth=0,relief="flat")style.configure("Modern.Treeview.Heading",font=self.title_font,background="#404040",foreground="#ffffff",relief="flat",borderwidth=1)# 配置滚动条样式style.configure("Modern.Vertical.TScrollbar",background="#404040",troughcolor="#2b2b2b",borderwidth=0,arrowcolor="#ffffff",darkcolor="#404040",lightcolor="#404040")# 配置框架样式style.configure("Modern.TLabelframe",font=self.title_font,borderwidth=2,relief="solid",bordercolor="#404040")style.configure("Modern.TLabelframe.Label",font=self.title_font,foreground="#00d4aa",background="#1e1e1e")# 界面布局def _init_ui(self):"""现代化界面布局:return:"""# 创建主容器,添加内边距main_container = ttk.Frame(self.root, padding=20)main_container.grid(row=0, column=0, sticky=(N, W, E, S), padx=10, pady=10)# 配置主容器的网格权重main_container.columnconfigure(0, weight=1)main_container.rowconfigure(3, weight=1) # 让结果表格区域可扩展# 1、用户输入区域 - 使用现代化卡片样式input_frame = ttk.Labelframe(main_container, text="📥 M3U8 地址输入",style="Modern.TLabelframe", padding=15)input_frame.grid(row=0, column=0, sticky=(W, E), pady=(0, 15))input_frame.columnconfigure(1, weight=1)self.parse_user_input_layout(input_frame)# 2、软件配置区域config_frame = ttk.Labelframe(main_container, text="⚙️ 软件配置参数",style="Modern.TLabelframe", padding=15)config_frame.grid(row=1, column=0, sticky=(W, E), pady=(0, 15))config_frame.columnconfigure(1, weight=1)config_frame.columnconfigure(3, weight=1)config_frame.columnconfigure(5, weight=1)self.parse_settings_layout(config_frame)# 3、解密算法区域encrypt_frame = ttk.Labelframe(main_container, text="🔐 解密算法配置",style="Modern.TLabelframe", padding=15)encrypt_frame.grid(row=2, column=0, sticky=(W, E), pady=(0, 15))self.parse_aes_encrypt_layout(encrypt_frame)# 4、解析结果表格区域result_frame = ttk.Labelframe(main_container, text="📊 M3U8 解析结果",style="Modern.TLabelframe", padding=15)result_frame.grid(row=3, column=0, sticky=(N, W, E, S), pady=(0, 15))result_frame.columnconfigure(0, weight=1)result_frame.rowconfigure(0, weight=1)self.parse_m3u8_url_result_layout(result_frame)# 5、操作控制区域control_frame = ttk.Frame(main_container, padding=10)control_frame.grid(row=4, column=0, sticky=(W, E))control_frame.columnconfigure(0, weight=1)self.parse_login_info_layout(control_frame)# UI界面:用户的输入区域def parse_user_input_layout(self, frame):"""现代化用户输入区域:param frame::return:"""# 配置网格权重frame.columnconfigure(1, weight=1)# m3u8地址输入标签url_label = ttk.Label(frame, text='🔗 M3U8 地址:', style="Title.TLabel")url_label.grid(row=0, column=0, sticky=W, padx=(0, 15), pady=(5, 5))# m3u8地址输入框self.start_url = ttk.Entry(frame, style="Modern.TEntry", font=self.normal_font)self.start_url.grid(row=0, column=1, sticky=(W, E), padx=(0, 15), pady=(5, 5))self.start_url.insert(0, "请输入 M3U8 播放列表地址...")# 绑定焦点事件来实现占位符效果self.start_url.bind('<FocusIn>', self.on_url_focus_in)self.start_url.bind('<FocusOut>', self.on_url_focus_out)# 按钮容器button_frame = ttk.Frame(frame)button_frame.grid(row=0, column=2, padx=(0, 0), pady=(5, 5))# 解析按钮parse_btn = ttk.Button(button_frame, text='🚀 开始解析', style="Modern.TButton",bootstyle="success",command=lambda: Thread(target=self.parse_start_run, daemon=True).start())parse_btn.grid(row=0, column=0, padx=(0, 10))# 清空按钮clear_btn = ttk.Button(button_frame, text='🚮 清空界面', style="Modern.TButton",bootstyle="warning",command=lambda: Thread(target=self.parse_clear_ui, daemon=True).start())clear_btn.grid(row=0, column=1)def on_url_focus_in(self, event):"""输入框获得焦点时清除占位符"""if self.start_url.get() == "请输入 M3U8 播放列表地址...":self.start_url.delete(0, 'end')def on_url_focus_out(self, event):"""输入框失去焦点时恢复占位符"""if not self.start_url.get():self.start_url.insert(0, "请输入 M3U8 播放列表地址...")# UI界面:加载软件配置区域def parse_settings_layout(self, frame):"""现代化软件配置区域:param frame::return:"""# 配置网格权重frame.columnconfigure(1, weight=1)frame.columnconfigure(3, weight=1)frame.columnconfigure(5, weight=1)# 第一行:HTTP 请求头配置# Referer 配置referer_label = ttk.Label(frame, text='🌐 Referer:', font=self.normal_font)referer_label.grid(row=0, column=0, sticky=W, padx=(0, 10), pady=(5, 5))self.referer = ttk.Entry(frame, style="Modern.TEntry", font=self.normal_font)self.referer.grid(row=0, column=1, sticky=(W, E), padx=(0, 15), pady=(5, 5))# Origin 配置origin_label = ttk.Label(frame, text='🔗 Origin:', font=self.normal_font)origin_label.grid(row=0, column=2, sticky=W, padx=(0, 10), pady=(5, 5))self.Origin = ttk.Entry(frame, style="Modern.TEntry", font=self.normal_font)self.Origin.grid(row=0, column=3, sticky=(W, E), padx=(0, 15), pady=(5, 5))# Host 配置host_label = ttk.Label(frame, text='🏠 Host:', font=self.normal_font)host_label.grid(row=0, column=4, sticky=W, padx=(0, 10), pady=(5, 5))self.host = ttk.Entry(frame, style="Modern.TEntry", font=self.normal_font)self.host.grid(row=0, column=5, sticky=(W, E), padx=(0, 0), pady=(5, 5))# 第二行:文件和路径配置# 保存路径选择path_btn = ttk.Button(frame, text='📁 保存路径', style="Modern.TButton",bootstyle="info",command=lambda: Thread(target=self.parse_open_file_os_path, daemon=True).start())path_btn.grid(row=1, column=0, sticky=W, padx=(0, 10), pady=(10, 5))self.down_os_path = ttk.Entry(frame, style="Modern.TEntry", font=self.normal_font)self.down_os_path.grid(row=1, column=1, sticky=(W, E), padx=(0, 15), pady=(10, 5))# 合并引擎选择engine_btn = ttk.Button(frame, text='⚙️ 合并引擎', style="Modern.TButton",bootstyle="info",command=lambda: Thread(target=self.parse_open_exe_file, daemon=True).start())engine_btn.grid(row=1, column=2, sticky=W, padx=(0, 10), pady=(10, 5))self.open_exe_file = ttk.Entry(frame, style="Modern.TEntry", font=self.normal_font)self.open_exe_file.grid(row=1, column=3, sticky=(W, E), padx=(0, 15), pady=(10, 5))# 文件名配置filename_label = ttk.Label(frame, text='📄 保存文件名:', font=self.normal_font)filename_label.grid(row=1, column=4, sticky=W, padx=(0, 10), pady=(10, 5))self.excel_path = ttk.Entry(frame, style="Modern.TEntry", font=self.normal_font)self.excel_path.grid(row=1, column=5, sticky=(W, E), padx=(0, 0), pady=(10, 5))# UI界面:解密区域布局def parse_aes_encrypt_layout(self, frame):"""现代化解密区域:param frame::return:"""# 配置网格权重frame.columnconfigure(1, weight=1)frame.columnconfigure(3, weight=1)frame.columnconfigure(5, weight=1)# 第一行:解密算法和密钥配置# 解密算法algorithm_label = ttk.Label(frame, text='🔐 解密算法:', font=self.normal_font)algorithm_label.grid(row=0, column=0, sticky=W, padx=(0, 10), pady=(5, 5))self.decrypt_module = ttk.Entry(frame, style="Modern.TEntry", font=self.normal_font)self.decrypt_module.grid(row=0, column=1, sticky=(W, E), padx=(0, 15), pady=(5, 5))self.decrypt_module.insert(0, "AES-128")# 解密密钥key_label = ttk.Label(frame, text='🔑 解密密钥Key:', font=self.normal_font)key_label.grid(row=0, column=2, sticky=W, padx=(0, 10), pady=(5, 5))self.key = ttk.Entry(frame, style="Modern.TEntry", font=self.normal_font)self.key.grid(row=0, column=3, sticky=(W, E), padx=(0, 15), pady=(5, 5))# 解密向量iv_label = ttk.Label(frame, text='🎯 解密向量iv:', font=self.normal_font)iv_label.grid(row=0, column=4, sticky=W, padx=(0, 10), pady=(5, 5))self.iv = ttk.Entry(frame, style="Modern.TEntry", font=self.normal_font)self.iv.grid(row=0, column=5, sticky=(W, E), padx=(0, 0), pady=(5, 5))# 第二行:超时和线程配置# 超时设置timeout_label = ttk.Label(frame, text='⏱️ 超时设置:', font=self.normal_font)timeout_label.grid(row=1, column=0, sticky=W, padx=(0, 10), pady=(10, 5))timeout_frame = ttk.Frame(frame)timeout_frame.grid(row=1, column=1, sticky=W, padx=(0, 15), pady=(10, 5))self.time_output = ttk.Entry(timeout_frame, style="Modern.TEntry", font=self.normal_font, width=8)self.time_output.grid(row=0, column=0, padx=(0, 5))self.time_output.insert(0, '5')ttk.Label(timeout_frame, text='秒', font=self.normal_font).grid(row=0, column=1)# 并发线程数thread_label = ttk.Label(frame, text='🧵 并发线程:', font=self.normal_font)thread_label.grid(row=1, column=2, sticky=W, padx=(0, 10), pady=(10, 5))thread_frame = ttk.Frame(frame)thread_frame.grid(row=1, column=3, sticky=W, padx=(0, 15), pady=(10, 5))# 添加线程数配置self.thread_num = ttk.Entry(thread_frame, style="Modern.TEntry", font=self.normal_font, width=8)self.thread_num.grid(row=0, column=0, padx=(0, 5))self.thread_num.insert(0, '5')ttk.Label(thread_frame, text='(最大10)', font=self.small_font, foreground='gray').grid(row=0, column=1)# UI界面:解析m3u8结果区域def parse_m3u8_url_result_layout(self, frame):"""现代化表格区域:param frame::return:"""# 定义现代化表格控件self.user_treeview = ttk.Treeview(frame,columns=['num', 'www', 'ts_name', 'code', 'sign'],show='headings',height=15,style="Modern.Treeview")# 配置列宽和对齐方式self.user_treeview.column('num', anchor='center', width=80, minwidth=60)self.user_treeview.column('www', anchor='w', width=400, minwidth=300)self.user_treeview.column('ts_name', anchor='center', width=250, minwidth=200)self.user_treeview.column('code', anchor='center', width=120, minwidth=100)self.user_treeview.column('sign', anchor='center', width=120, minwidth=100)# 设置现代化列标题self.user_treeview.heading('num', text='📊 序号')self.user_treeview.heading('www', text='🌐 资源地址')self.user_treeview.heading('ts_name', text='📁 TS文件名')self.user_treeview.heading('code', text='📈 下载状态')self.user_treeview.heading('sign', text='🔄 处理状态')# 表格在界面的显示位置self.user_treeview.grid(row=0, column=0, sticky=(N, W, E, S), padx=(0, 5), pady=0)# 添加现代化滚动条user_scroll_bar = ttk.Scrollbar(frame, orient='vertical',command=self.user_treeview.yview,style="Modern.Vertical.TScrollbar")# 滚动条绑定表格self.user_treeview.configure(yscrollcommand=user_scroll_bar.set)# 定义滚动条的位置user_scroll_bar.grid(row=0, column=1, sticky='ns', padx=(0, 0))# 添加表格右键菜单self.create_context_menu()# UI界面:右键功能菜单def create_context_menu(self):"""创建表格右键菜单"""self.context_menu = tk.Menu(self.root, tearoff=0)self.context_menu.add_command(label="📋 复制链接", command=self.copy_selected_url)self.context_menu.add_command(label="🔄 重新下载", command=self.retry_download)self.context_menu.add_separator()self.context_menu.add_command(label="🚮 删除选中", command=self.delete_selected)# 绑定右键事件self.user_treeview.bind("<Button-3>", self.show_context_menu)# 功能函数:显示右键菜单def show_context_menu(self, event):"""显示右键菜单"""try:self.context_menu.tk_popup(event.x_root, event.y_root)finally:self.context_menu.grab_release()# 功能函数:复制选中URLdef copy_selected_url(self):"""复制选中的URL"""selection = self.user_treeview.selection()if selection:item = self.user_treeview.item(selection[0])if item['values'][2].startswith('http'):url = item['values'][2] # 获取URL列else:url = item['values'][1] + item['values'][2] # 获取url列self.root.clipboard_clear()self.root.clipboard_append(url)self.parse_log_to_show("已复制链接到剪贴板")# 功能函数:重新下载def retry_download(self):"""重新下载选中项"""selection = self.user_treeview.selection()if selection:self.parse_log_to_show("开始重新下载选中项目")# 功能函数:删除选中项def delete_selected(self):"""删除选中项"""selection = self.user_treeview.selection()if selection:for item in selection:self.user_treeview.delete(item)self.parse_log_to_show(f"已删除 {len(selection)} 个选中项")# 功能函数:清空界面def parse_clear_ui(self):""":return:"""# 清空m3u8界面self.user_treeview.delete(*self.user_treeview.get_children())# 清空日志界面self.text_box_output.delete(1.0, 'end')# 功能函数:选择下载路径def parse_open_file_os_path(self):""":return:"""# 弹出保存文件对话框file_path = filedialog.askdirectory(title="选择保存文件夹")if not file_path:returnself.down_os_path.delete(0, 'end')self.down_os_path.insert(0, file_path)# 功能函数:选择合并引擎def parse_open_exe_file(self):""":return:"""# 弹出选择exe文件对话框file_path = filedialog.askopenfilename(title="选择exe文件",filetypes=[("可执行文件", "*.exe"), ("所有文件", "*.*")])if not file_path:returnself.open_exe_file.delete(0, 'end')self.open_exe_file.insert(0, file_path)# 功能函数:同步更新treeviewdef process_task_queue(self):""":return:"""with self.task_lock:try:while True:# 获取队列数据task = self.task_queue.get_nowait()if task.get('type') == 'spider_result':# 获取表格数据数量data_num = len(self.user_treeview.get_children())data_list = task.get('data')data_list[0] = data_num + 1# 执行表格更新操作self.user_treeview.insert("", "end", values=data_list)self.user_treeview.yview_moveto(1) # 滚动到底部except queue.Empty:pass# 每500毫秒监控更新一次self.root.after(500, self.process_task_queue)# 子frame函数:软件提示与下载按钮def parse_login_info_layout(self, frame):"""现代化操作控制区域:return:"""# 配置网格权重frame.columnconfigure(0, weight=1)# 软件运行日志区域info_data_view = ttk.Labelframe(frame, text="📝 操作日志",style="Modern.TLabelframe", padding=10)info_data_view.grid(row=0, column=0, sticky=(N, W, E, S), padx=(0, 15), pady=(0, 0))info_data_view.columnconfigure(0, weight=1)info_data_view.rowconfigure(0, weight=1)self.parse_info_data_view(info_data_view)# 操作按钮区域button_frame = ttk.Frame(frame)button_frame.grid(row=0, column=1, sticky=(N, E), padx=(0, 0), pady=(0, 0))# 下载按钮download_btn = ttk.Button(button_frame, text='🚀 开始下载',style="Modern.TButton",bootstyle="success",command=self.parse_down_load_ts_to_loca)download_btn.grid(row=0, column=0, pady=(0, 10), sticky=(W, E))# 合并按钮merge_btn = ttk.Button(button_frame, text='🔗 合并文件',style="Modern.TButton",bootstyle="info",command=lambda: Thread(target=self.parse_hb_data, daemon=True).start())merge_btn.grid(row=1, column=0, pady=(0, 10), sticky=(W, E))# 暂停/继续按钮self.pause_btn = ttk.Button(button_frame, text='⏸️ 暂停下载',style="Modern.TButton",bootstyle="warning",command=self.toggle_download)self.pause_btn.grid(row=2, column=0, pady=(0, 10), sticky=(W, E))# 停止按钮stop_btn = ttk.Button(button_frame, text='⏹️ 停止下载',style="Modern.TButton",bootstyle="danger",command=self.stop_download)stop_btn.grid(row=3, column=0, sticky=(W, E))# 设置按钮最小宽度for i in range(4):button_frame.grid_rowconfigure(i, minsize=40)# 功能函数:暂停/继续下载def toggle_download(self):"""切换下载状态"""if not self.is_downloading:self.parse_log_to_show("当前没有正在进行的下载任务")returnif self.pause_btn.cget('text') == '⏸️ 暂停下载':# 暂停下载self.is_paused = Trueself.pause_btn.configure(text='▶️ 继续下载')self.parse_log_to_show("下载已暂停")else:# 继续下载self.is_paused = Falseself.pause_btn.configure(text='⏸️ 暂停下载')self.parse_log_to_show("下载已继续")# 功能函数:停止下载def stop_download(self):"""停止下载"""if not self.is_downloading:self.parse_log_to_show("当前没有正在进行的下载任务")return# 设置停止标志self.download_stopped = Trueself.is_downloading = Falseself.is_paused = False# 关闭线程池if self.executor:self.executor.shutdown(wait=False)self.executor = None# 清空下载队列while not self.task_url_queue.empty():try:self.task_url_queue.get_nowait()except queue.Empty:break# 重置按钮状态self.pause_btn.configure(text='⏸️ 暂停下载')self.parse_log_to_show("下载已停止,线程池已关闭")# 功能函数:软件提示def parse_info_data_view(self, frame):""":param frame::return:"""self.text_box_output = tk.Text(frame,width=75,height=2,wrap=tk.WORD,font=self.normal_font,padx=8,pady=8,bg='#2b2b2b',fg='#ffffff',insertbackground='#ffffff',selectbackground='#0078d4',borderwidth=1,relief='solid')self.text_box_output.grid(row=0, column=0, sticky=(N, S, E, W))# 滚动条scroll_bar = ttk.Scrollbar(frame, orient='vertical', command=self.text_box_output.yview)self.text_box_output.configure(yscrollcommand=scroll_bar.set)scroll_bar.grid(row=0, column=1, sticky='ns')# 初始说明文本text1 = """提示:准备就绪!"""self.text_box_output.tag_config("time_color", foreground="yellow")self.text_box_output.insert(tk.END, f"{text1}\n\n", "time_color")# 日志显示def parse_log_to_show(self, text, co=False):"""日志显示"""self.text_box_output.tag_config("time", foreground="yellow")self.text_box_output.tag_config("error", foreground="red")time_str = f"[{datetime.now().strftime('%H:%M:%S')}] "self.text_box_output.insert(tk.END, time_str, "time")if co:self.text_box_output.insert(tk.END, f"{text}\n", "error")else:self.text_box_output.insert(tk.END, f"{text}\n")self.text_box_output.see(tk.END) # 滚动到最新内容# 启动函数def run(self):self.root.place_window_center()# 允许调整大小,提升用户体验self.root.resizable(True, True)# 设置最小窗口尺寸self.root.minsize(1200, 800)# 设置最大窗口尺寸(可选)self.root.maxsize(1920, 1080)self.root.mainloop()"""--------------------------------------------------------------"""# 功能函数:浏览器请求头def parse_browser_headers(self):Origin = self.Origin.get().strip()Referer = self.referer.get().strip()Host = self.host.get().strip()# Origin = 'https://www.vvvdj.com/'# Referer = 'https://www.vvvdj.com/'headers = {"Accept": "*/*","Accept-Language": "zh-CN,zh;q=0.9","Cache-Control": "no-cache","Connection": "keep-alive","Origin": Origin,"Pragma": "no-cache","Referer": Referer,"Sec-Fetch-Dest": "empty","Sec-Fetch-Mode": "cors","Sec-Fetch-Site": "same-site","User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36","sec-ch-ua": "\"Not;A=Brand\";v=\"99\", \"Google Chrome\";v=\"139\", \"Chromium\";v=\"139\"","sec-ch-ua-mobile": "?0","sec-ch-ua-platform": "\"Windows\""}if Host:headers["Host"] = Hostreturn headers# 功能函数:开始解析def parse_start_run(self):"""解析M3U8文件,支持加密内容:return:"""try:# 获取用户输入的URLurl = self.start_url.get().strip()# url = 'https://tspc.vvvdj.com/c1/2024/08/273404-8c4c98/273404.m3u8?upt=8ea4097f1760025599'if not url or url == "请输入 M3U8 播放列表地址...":self.parse_log_to_show("请输入有效的M3U8链接")returnself.parse_log_to_show(f"开始解析M3U8链接: {url}")# 清空现有数据self.user_treeview.delete(*self.user_treeview.get_children())headers = self.parse_browser_headers()# 发送请求获取M3U8内容response = requests.get(url, headers=headers, timeout=30)response.raise_for_status()m3u8_content = response.textself.parse_log_to_show("M3U8文件获取成功,开始解析...")# 解析加密信息encryption_info = self.parse_encryption_info(m3u8_content, url, headers)# 正则提取TS地址ts_list = re.findall(r'^[^#].*\.ts[^\s]*', m3u8_content, re.MULTILINE)if not ts_list:self.parse_log_to_show("未找到TS文件,请检查M3U8链接是否正确")return# 获取基础URLbase_url = '/'.join(url.split('/')[:-1]) + '/'self.parse_log_to_show(f"找到 {len(ts_list)} 个TS文件")if encryption_info:self.parse_log_to_show(f"检测到加密内容: {encryption_info['method']}")if encryption_info.get('key_url'):self.parse_log_to_show(f"密钥地址: {encryption_info['key_url']}")if encryption_info.get('iv'):self.parse_log_to_show(f"初始化向量: {encryption_info['iv']}")# 构建渲染treeviewfor i, ts_file in enumerate(ts_list, 1):# 检查是否为相对路径if not ts_file.startswith('http'):ts_url = base_url + ts_fileelse:ts_url = ts_filestatus = '待下载'sign = '未加密'if encryption_info:status = '待下载'sign = '加密'aef_list = {'type': 'spider_result','data': [str(i), base_url, ts_url, status, sign],'encryption': encryption_info # 添加加密信息}self.task_queue.put(aef_list)self.parse_log_to_show("M3U8解析完成!")except requests.exceptions.RequestException as e:self.parse_log_to_show(f"网络请求失败: {str(e)}")except Exception as e:self.parse_log_to_show(f"解析M3U8文件时出错: {str(e)}")# 功能函数: 解析M3U8文件中的加密信息def parse_encryption_info(self, m3u8_content, base_url, headers):"""解析M3U8文件中的加密信息,支持手动输入的密钥和IV:param m3u8_content: M3U8文件内容:param base_url: 基础URL:param headers: 请求头:return: 加密信息字典或None"""try:# 查找EXT-X-KEY标签key_pattern = r'#EXT-X-KEY:(.+)'key_matches = re.findall(key_pattern, m3u8_content)# 检查用户是否手动输入了加密信息manual_key = self.key.get().strip()manual_iv = self.iv.get().strip()manual_method = self.decrypt_module.get().strip()# 如果没有找到EXT-X-KEY标签且没有手动输入,返回Noneif not key_matches and not manual_key:return Noneencryption_info = {}# 优先使用手动输入的加密方法if manual_method:encryption_info['method'] = manual_method.upper()elif key_matches:# 解析最后一个KEY标签(通常是当前使用的)key_line = key_matches[-1]method_match = re.search(r'METHOD=([^,]+)', key_line)if method_match:encryption_info['method'] = method_match.group(1).strip('"')# 处理密钥if manual_key:# 使用手动输入的密钥try:if manual_key.startswith('0x'):# 十六进制格式encryption_info['key'] = bytes.fromhex(manual_key[2:])elif len(manual_key) == 32: # 假设是十六进制字符串encryption_info['key'] = bytes.fromhex(manual_key)else:# 假设是字符串,转换为bytesencryption_info['key'] = manual_key.encode('utf-8')self.parse_log_to_show("使用手动输入的密钥")except Exception as e:self.parse_log_to_show(f"解析手动输入密钥失败: {str(e)}")return Noneelif key_matches:# 从M3U8文件解析密钥URIkey_line = key_matches[-1]uri_match = re.search(r'URI="([^"]+)"', key_line)if uri_match:key_uri = uri_match.group(1)# 处理相对路径if not key_uri.startswith('http'):key_uri = '/'.join(base_url.split('/')[:-1]) + '/' + key_uriencryption_info['key_url'] = key_uri# 尝试获取密钥try:key_response = requests.get(key_uri, headers=headers, timeout=10)key_response.raise_for_status()encryption_info['key'] = key_response.contentself.parse_log_to_show(f"成功从URI获取加密密钥: {key_uri}")except Exception as e:self.parse_log_to_show(f"获取密钥失败: {str(e)}")return None# 处理IV(初始化向量)if manual_iv:# 使用手动输入的IVtry:if manual_iv.startswith('0x'):encryption_info['iv'] = bytes.fromhex(manual_iv[2:])elif len(manual_iv) == 32: # 假设是十六进制字符串encryption_info['iv'] = bytes.fromhex(manual_iv)else:# 假设是字符串,转换为bytes并填充到16字节iv_bytes = manual_iv.encode('utf-8')if len(iv_bytes) < 16:iv_bytes = iv_bytes + b'\x00' * (16 - len(iv_bytes))elif len(iv_bytes) > 16:iv_bytes = iv_bytes[:16]encryption_info['iv'] = iv_bytesself.parse_log_to_show("使用手动输入的IV")except Exception as e:self.parse_log_to_show(f"解析手动输入IV失败: {str(e)}")elif key_matches:# 从M3U8文件解析IVkey_line = key_matches[-1]iv_match = re.search(r'IV=0x([A-Fa-f0-9]+)', key_line)if iv_match:encryption_info['iv'] = bytes.fromhex(iv_match.group(1))self.parse_log_to_show("从M3U8文件解析到IV")# 确保有密钥才返回加密信息if encryption_info.get('key'):return encryption_infoelse:self.parse_log_to_show("未找到有效的加密密钥")return Noneexcept Exception as e:self.parse_log_to_show(f"解析加密信息时出错: {str(e)}")return None# 功能函数:下载tsdef parse_down_load_ts_to_loca(self):""":return:"""# 检查是否已经在下载if self.is_downloading:self.parse_log_to_show("下载任务正在进行中,请等待完成或停止当前任务")return# 检查是否有数据可下载if not self.user_treeview.get_children():self.parse_log_to_show("没有可下载的文件,请先解析M3U8链接")return# 重置下载状态self.download_stopped = Falseself.is_paused = Falseself.is_downloading = Trueself.completion_dialog_shown = False # 重置完成对话框标志# 获取ts的保存路径os_path = self.down_os_path.get().strip()if not os_path:# 取默认路径os_path = str(datetime.now().strftime('%Y%m%d_%H%M%S'))path = os.getcwd() + f"/{os_path}/"else:path = os_path# 创建目录(如果不存在)try:if not os.path.exists(path):os.makedirs(path)self.download_path = pathexcept Exception as e:self.parse_log_to_show(f"创建下载目录失败: {str(e)}")self.is_downloading = Falsereturn# 获取treeview数据treeview_list = [[str(i) for i in self.user_treeview.item(item)["values"]] for item inself.user_treeview.get_children()]# 获取treeview的iditems = self.user_treeview.get_children()# 清空任务队列while not self.task_url_queue.empty():try:self.task_url_queue.get_nowait()except queue.Empty:break# 先将地址,放到任务队列中for ts_url in treeview_list:if self.download_stopped:breakaef_data = {'path': path,'ts_data': ts_url,'item': items}self.task_url_queue.put(aef_data)# 获取线程数n = int(self.thread_num.get().strip()) if self.thread_num.get().strip() else 5# 创建新的线程池if self.executor:self.executor.shutdown(wait=False)self.executor = ThreadPoolExecutor(max_workers=n)self.parse_log_to_show(f"开始下载,使用 {n} 个线程,保存路径: {path}")# 启动线程池中的工作线程for _ in range(n):if not self.download_stopped:self.executor.submit(self.parse_spider_worker_start) # 提交任务到线程池# 爬虫任务启动:工作线程启动、循环def parse_spider_worker_start(self):"""工作线程启动、循环:return:"""while not self.download_stopped: # 根据停止标志控制循环try:# 检查是否暂停while self.is_paused and not self.download_stopped:threading.Event().wait(0.5) # 暂停时等待0.5秒if self.download_stopped:break# 从队列获取任务(最多等待1秒)task = self.task_url_queue.get(timeout=1)# 再次检查是否停止或暂停if self.download_stopped:breakself.parse_spider_process_task(task)except queue.Empty:# 队列为空,检查是否所有任务完成if self.task_url_queue.empty():breakcontinue # 队列为空时继续尝试# 线程结束时检查是否所有线程都完成self.check_download_completion()# 执行下载tsdef parse_spider_process_task(self, task):"""下载并处理TS文件,支持加密解密:param task::return:"""# 检查是否停止下载if self.download_stopped:returnitems = task['item']# 提取ts地址t_url = task['ts_data'][2]if t_url.startswith('http'):ts_url = t_urlelse:ts_url = task['ts_data'][1] + task['ts_data'][2]# 提取表格id_id = int(task['ts_data'][0]) - 1# 获取加密信息encryption_info = task.get('encryption')try:# 更新状态为正在下载current_values = task['ts_data'].copy()if encryption_info:current_values[3] = '正在下载(加密)...'else:current_values[3] = '正在下载...'self.user_treeview.item(items[_id], values=current_values)headers = self.parse_browser_headers()# 获取超时时间time_int = int(self.time_output.get().strip()) if self.time_output.get().strip() else 10# 再次检查是否停止if self.download_stopped:return# 获取ts的二进制资源response = requests.get(ts_url, headers=headers, timeout=time_int)response.raise_for_status()ts_data = response.content# 检查是否停止if self.download_stopped:return# 如果有加密信息,进行解密if encryption_info and encryption_info.get('key'):try:ts_data = self.decrypt_ts_data(ts_data, encryption_info, _id)current_values[4] = '解密完成!'self.user_treeview.item(items[_id], values=current_values)except Exception as decrypt_error:self.parse_log_to_show(f"第{_id + 1}行,解密失败: {str(decrypt_error)}")current_values[4] = '解密失败!'self.user_treeview.item(items[_id], values=current_values)return# 文件保存命名name = ts_url.split('/')[-1].replace('-', "_")if not name.endswith('.ts'):name += '.ts'# 下载ts资源file_path = os.path.join(task['path'], name)with open(file_path, 'wb') as f:f.write(ts_data)# 更新状态为下载完成if encryption_info:current_values[3] = '下载完成(已解密)!'else:current_values[3] = '下载完成!'self.user_treeview.item(items[_id], values=current_values)# 日志输出if encryption_info:text = f"第{_id + 1}行,加密TS文件下载并解密完成!"else:text = f"第{_id + 1}行,TS文件下载完成!"self.parse_log_to_show(text, True)except Exception as e:if not self.download_stopped:# 更新状态为下载失败current_values = task['ts_data'].copy()current_values[3] = '下载失败!'self.user_treeview.item(items[_id], values=current_values)# 日志输出错误text = f"第{_id + 1}行,下载失败: {str(e)}"self.parse_log_to_show(text, True)# 解密TS数据def decrypt_ts_data(self, encrypted_data, encryption_info, segment_index):"""解密TS数据:param encrypted_data: 加密的TS数据:param encryption_info: 加密信息:param segment_index: 片段索引:return: 解密后的数据"""try:method = encryption_info.get('method', '').upper()key = encryption_info.get('key')iv = encryption_info.get('iv')if not key:raise ValueError("缺少解密密钥")if method == 'AES-128':# 如果没有指定IV,使用片段索引作为IVif not iv:iv = segment_index.to_bytes(16, byteorder='big')elif len(iv) < 16:# 如果IV长度不足16字节,用0填充iv = iv + b'\x00' * (16 - len(iv))# 创建AES解密器cipher = AES.new(key, AES.MODE_CBC, iv)# 解密数据decrypted_data = cipher.decrypt(encrypted_data)# 移除PKCS7填充try:decrypted_data = unpad(decrypted_data, AES.block_size)except ValueError:# 如果无法移除填充,可能数据本身没有填充passreturn decrypted_dataelif method == 'NONE':# 无加密return encrypted_dataelse:raise ValueError(f"不支持的加密方法: {method}")except Exception as e:raise Exception(f"解密失败: {str(e)}")# 检查所有TS片段下载是否完成def check_download_completion(self):"""检查下载是否完成"""# 使用线程安全的方式检查def check_completion():if self.task_url_queue.empty() and not self.download_stopped and not self.completion_dialog_shown:# 所有任务完成self.is_downloading = Falseself.completion_dialog_shown = True # 设置标志防止重复弹窗self.parse_log_to_show("所有下载任务已完成!")# 询问是否合并文件if messagebox.askyesno("下载完成", "所有文件下载完成!是否立即合并文件?"):Thread(target=self.parse_hb_data, daemon=True).start()# 在主线程中执行检查self.root.after(100, check_completion)# 执行多个ts文件的合并def parse_hb_data(self):"""合并TS文件为完整视频"""try:# 检查是否有下载路径if not self.download_path or not os.path.exists(self.download_path):self.parse_log_to_show("错误:找不到下载文件夹")return# 获取合并引擎路径merge_engine = self.open_exe_file.get().strip()# 获取输出文件名output_filename = self.excel_path.get().strip()if not output_filename:output_filename = f"merged_video_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mp4"elif not output_filename.endswith('.mp4'):output_filename += '.mp4'output_path = os.path.join(self.download_path, output_filename)self.parse_log_to_show("开始合并文件...")# 获取所有TS文件ts_files = []for file in os.listdir(self.download_path):if file.endswith('.ts'):ts_files.append(os.path.join(self.download_path, file))if not ts_files:self.parse_log_to_show("错误:没有找到TS文件")return# 按文件名排序ts_files.sort()self.parse_log_to_show(f"找到 {len(ts_files)} 个TS文件,开始合并...")# 方法1:使用合并引擎合并(如果有指定合并引擎)if merge_engine and os.path.exists(merge_engine):self.merge_with_merge_engine(ts_files, output_path, merge_engine)else:# 方法2:直接二进制合并self.merge_with_binary(ts_files, output_path)except Exception as e:self.parse_log_to_show(f"合并文件时出错: {str(e)}")# 使用合并引擎合并文件def merge_with_merge_engine(self, ts_files, output_path, merge_engine_path):"""使用合并引擎合并TS文件"""try:# 创建文件列表list_file = os.path.join(os.path.dirname(output_path), "filelist.txt")with open(list_file, 'w', encoding='utf-8') as f:for ts_file in ts_files:f.write(f"file '{ts_file}'\n")# 构建FFmpeg命令cmd = f'"{merge_engine_path}" -f concat -safe 0 -i "{list_file}" -c copy "{output_path}"'self.parse_log_to_show("使用合并引擎合并文件...")# 执行命令result = os.system(cmd)# 删除临时文件列表if os.path.exists(list_file):os.remove(list_file)if result == 0:self.parse_log_to_show(f"合并成功!文件保存为: {output_path}")# 询问是否删除原始TS文件if messagebox.askyesno("合并完成", "合并成功!是否删除原始TS文件?"):self.cleanup_ts_files(ts_files)else:self.parse_log_to_show("合并引擎合并失败,尝试二进制合并...")self.merge_with_binary(ts_files, output_path)except Exception as e:self.parse_log_to_show(f"合并引擎合并出错: {str(e)},尝试二进制合并...")self.merge_with_binary(ts_files, output_path)# 使用二进制方式合并文件def merge_with_binary(self, ts_files, output_path):"""使用二进制方式合并TS文件"""try:self.parse_log_to_show("使用二进制方式合并文件...")with open(output_path, 'wb') as output_file:for i, ts_file in enumerate(ts_files):self.parse_log_to_show(f"合并进度: {i + 1}/{len(ts_files)}")with open(ts_file, 'rb') as input_file:output_file.write(input_file.read())self.parse_log_to_show(f"二进制合并成功!文件保存为: {output_path}")# 询问是否删除原始TS文件if messagebox.askyesno("合并完成", "合并成功!是否删除原始TS文件?"):self.cleanup_ts_files(ts_files)except Exception as e:self.parse_log_to_show(f"二进制合并出错: {str(e)}")# 清理TS文件def cleanup_ts_files(self, ts_files):"""清理TS文件"""try:deleted_count = 0for ts_file in ts_files:if os.path.exists(ts_file):os.remove(ts_file)deleted_count += 1self.parse_log_to_show(f"已删除 {deleted_count} 个TS文件")except Exception as e:self.parse_log_to_show(f"删除TS文件时出错: {str(e)}")if __name__ == '__main__':s = Spider()s.run()
运行效果如图: