From 5338058b5a1a1073fb9b45466fcd406ec84a54d3 Mon Sep 17 00:00:00 2001 From: pop-tianlang <2102679387@qq.com> Date: Sat, 7 Feb 2026 22:23:36 +0800 Subject: [PATCH] =?UTF-8?q?fix:=E5=88=9D=E5=A7=8B=E5=8C=96=E4=BB=93?= =?UTF-8?q?=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- douyin_video_crawler.py | 304 +++++++++++++++++++++++ video_converter_ui.py | 524 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 828 insertions(+) create mode 100644 douyin_video_crawler.py create mode 100644 video_converter_ui.py diff --git a/douyin_video_crawler.py b/douyin_video_crawler.py new file mode 100644 index 0000000..f0dcd2f --- /dev/null +++ b/douyin_video_crawler.py @@ -0,0 +1,304 @@ +import time +import os +import requests +import sys +from DrissionPage import ChromiumOptions, ChromiumPage + + +class VideoDownloader: + """视频下载器类""" + + def __init__(self, video_dir): + """ + 初始化视频下载器 + + Args: + video_dir: 视频保存目录 + """ + self.video_dir = video_dir + + def save_video(self, video_url, aweme_id, video_title): + """ + 下载视频到本地 + + Args: + video_url: 视频下载地址 + aweme_id: 视频的唯一标识符 + video_title: 视频标题 + + Returns: + bool: 下载是否成功 + """ + try: + # 清理视频标题,移除非法字符 + def clean_filename(filename): + invalid_chars = '<>:"/\\|?*' + for char in invalid_chars: + filename = filename.replace(char, '_') + return filename[:50] # 限制文件名长度 + + cleaned_title = clean_filename(video_title) + + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36...', + 'Referer': 'https://www.douyin.com/' + } + + print(f"开始下载视频: {cleaned_title}") + print(f"视频ID: {aweme_id}") + print(f"下载地址: {video_url}") + + response = requests.get(video_url, headers=headers, stream=True, timeout=30) + + if response.status_code == 200: + video_path = os.path.join(self.video_dir, f'{cleaned_title}_{aweme_id}.mp4') + print(f"保存路径: {video_path}") + + total_size = int(response.headers.get('content-length', 0)) + downloaded_size = 0 + + with open(video_path, 'wb') as f: + for chunk in response.iter_content(chunk_size=1024*1024): + if chunk: + f.write(chunk) + downloaded_size += len(chunk) + if total_size > 0: + progress = (downloaded_size / total_size) * 100 + print(f"下载进度: {progress:.1f}%", end='\r') + + print(f"\n视频下载完成: {cleaned_title}") + + wait_time = time.time() % 2 + 1 + print(f"等待 {wait_time:.1f} 秒后继续...") + time.sleep(wait_time) + return True + else: + print(f"下载失败,状态码: {response.status_code}") + return False + except Exception as e: + print(f"下载视频时出错: {str(e)}") + return False + + +class VideoInfoExtractor: + """视频信息提取器类""" + + def __init__(self, video_dir): + """ + 初始化视频信息提取器 + + Args: + video_dir: 视频保存目录 + """ + self.downloader = VideoDownloader(video_dir) + + def save_video_info(self, video_data): + """ + 提取视频信息并下载视频 + + Args: + video_data: 包含视频信息的字典 + + Returns: + dict: 提取的视频信息字典 + """ + print("\n开始提取视频信息...") + + minutes = video_data['video']['duration'] // 1000 // 60 + seconds = video_data['video']['duration'] // 1000 % 60 + + video_url = video_data['video']['play_addr']['url_list'][0].replace('playwm', 'play') + video_title = video_data['desc'].strip().replace('\n', '') + aweme_id = video_data['aweme_id'] + + print(f"视频标题: {video_title}") + print(f"视频时长: {minutes}:{seconds:02d}") + print(f"作者: {video_data['author']['nickname'].strip()}") + print(f"粉丝数: {video_data['author']['follower_count']}") + print(f"点赞数: {video_data['statistics']['digg_count']}") + + video_dict = { + '用户名': video_data['author']['nickname'].strip(), + '用户uid': 'a' + str(video_data['author']['uid']), + '粉丝数量': video_data['author']['follower_count'], + '视频描述': video_title, + '视频标题': video_title, + '点赞数量': video_data['statistics']['digg_count'], + '视频awemeid': aweme_id, + '视频时长': f"{minutes}:{seconds:02d}", + '视频链接': video_url, + } + + print("开始下载视频...") + download_success = self.downloader.save_video(video_url, aweme_id, video_title) + + if download_success: + print("视频信息提取和下载完成!") + else: + print("视频下载失败,但信息已提取") + + return video_dict + + +class DouyinCrawler: + """抖音爬虫类""" + + def __init__(self, browser_path=None): + """ + 初始化抖音爬虫 + + Args: + browser_path: 浏览器路径,默认为 Edge 默认路径 + """ + if browser_path is None: + browser_path = '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge' + self.browser_path = browser_path + self.driver = None + + def setup_browser(self): + """配置并初始化浏览器""" + co = ChromiumOptions().set_browser_path(self.browser_path) + self.driver = ChromiumPage(co) + + def crawl_videos(self, keyword, video_dir): + """ + 爬取视频数据 + + Args: + keyword: 搜索关键词 + video_dir: 视频保存目录 + """ + print(f"\n=== 开始爬取关键词: {keyword} ===") + print(f"目标保存目录: {video_dir}") + + extractor = VideoInfoExtractor(video_dir) + + print("设置网络请求监听器...") + self.driver.listen.start('www.douyin.com/aweme/v1/web/search/item', method='GET') + + url = f'https://www.douyin.com/search/{keyword}?type=video' + print(f"打开搜索页面: {url}") + self.driver.get(url) + + data_list = [] + total_videos = 0 + + print("\n开始爬取视频数据...") + for page in range(10): + print(f"\n=== 第 {page + 1} 页 ===") + print("滚动到页面底部...") + self.driver.scroll.to_bottom() + + print("等待网络响应...") + resp = self.driver.listen.wait() + json_data = resp.response.body + + print(f"获取到 {len(json_data['data'])} 个视频数据") + + for json_aweme_info in json_data['data']: + data = extractor.save_video_info(json_aweme_info['aweme_info']) + data_list.append(data) + total_videos += 1 + + print(f"当前累计爬取: {total_videos} 个视频") + + if not json_data['has_more']: + print("已到达最后一页,停止爬取") + break + + wait_time = time.time() % 3 + 2 + print(f"等待 {wait_time:.1f} 秒后继续下一页爬取...") + time.sleep(wait_time) + + print(f"\n=== 爬取完成 ===") + print(f"总计爬取: {total_videos} 个视频") + + # 保存元数据 + if data_list: + VideoManager.save_metadata(data_list, video_dir) + else: + print("没有爬取到视频数据,跳过元数据保存") + + def close(self): + """关闭浏览器""" + if self.driver: + self.driver.quit() + + +class VideoManager: + """视频管理器类""" + + @staticmethod + def create_video_directory(keyword): + """ + 创建视频保存目录 + + Args: + keyword: 搜索关键词 + + Returns: + str: 视频保存目录路径 + """ + video_dir = f"./douyin_videos/{keyword}" + if not os.path.exists(video_dir): + print(f"创建视频保存目录: {video_dir}") + os.makedirs(video_dir) + else: + print(f"使用现有目录: {video_dir}") + return video_dir + + @staticmethod + def save_metadata(metadata_list, video_dir): + """ + 保存视频元数据到JSON文件 + + Args: + metadata_list: 视频元数据列表 + video_dir: 保存目录路径 + """ + import json + + metadata_file = os.path.join(video_dir, 'metaData.json') + print(f"\n开始保存元数据到: {metadata_file}") + print(f"共 {len(metadata_list)} 个视频的元数据") + + try: + # 添加保存时间 + metadata = { + 'save_time': time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), + 'video_count': len(metadata_list), + 'videos': metadata_list + } + + with open(metadata_file, 'w', encoding='utf-8') as f: + json.dump(metadata, f, ensure_ascii=False, indent=2) + + print(f"元数据保存成功!") + print(f"文件大小: {os.path.getsize(metadata_file) / 1024:.2f} KB") + except Exception as e: + print(f"保存元数据时出错: {str(e)}") + + +def main(): + """主函数""" + if len(sys.argv) < 2: + print("请提供关键词,例如:python pyauto.py 猫咪") + sys.exit(1) + + keyword = sys.argv[1] + + # 创建视频保存目录 + video_dir = VideoManager.create_video_directory(keyword) + + # 创建爬虫实例并开始爬取 + crawler = DouyinCrawler() + + try: + crawler.setup_browser() + crawler.crawl_videos(keyword, video_dir) + finally: + crawler.close() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/video_converter_ui.py b/video_converter_ui.py new file mode 100644 index 0000000..e98c01a --- /dev/null +++ b/video_converter_ui.py @@ -0,0 +1,524 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +视频格式转换工具(带GUI界面) +使用ffmpeg将指定目录下的全部视频转换为mp4格式的横屏1080p视频 +支持配置ffmpeg的大部分参数 +""" + +import os +import subprocess +import sys +import threading +import tkinter as tk +from tkinter import filedialog, messagebox, ttk +from concurrent.futures import ThreadPoolExecutor, as_completed + + +class VideoConverterUI: + def __init__(self, root): + self.root = root + self.root.title("视频格式转换工具") + self.root.geometry("800x800") + self.root.resizable(True, True) + + # 设置主题颜色 + self.bg_color = "#f0f0f0" # 主窗口背景色(浅灰) + self.accent_color = "#4CAF50" # 强调色:标题、悬停边框(绿色) + self.text_color = "#000000" # 普通文字颜色(黑色) + self.frame_color = "#ffffff" # 内部框架背景色(白色) + self.button_bg = "#4CAF50" # 按钮背景色(绿色) + self.button_fg = "#000000" # 按钮文字颜色(白色) + + # 变量 + self.input_dir = tk.StringVar() + self.output_dir = tk.StringVar(value="output") + self.threads = tk.StringVar(value="4") + self.ffmpeg_params = tk.StringVar() + self.is_converting = False + self.convert_thread = None + + # 创建界面 + self.create_widgets() + + def create_widgets(self): + # 设置窗口背景 + self.root.configure(bg=self.bg_color) + + # 主框架 + main_frame = ttk.Frame(self.root, padding="20", style="Main.TFrame") + main_frame.pack(fill=tk.BOTH, expand=True) + + # 设置样式 + self.setup_styles() + + # 标题 + title_label = ttk.Label(main_frame, text="视频格式转换工具", font=("Microsoft YaHei", 18, "bold"), style="Title.TLabel") + title_label.pack(pady=10) + + # 输入目录 + input_frame = ttk.LabelFrame(main_frame, text="输入设置", padding="15") + input_frame.pack(fill=tk.X, pady=10) + + # 输入目录行 + input_row = ttk.Frame(input_frame) + input_row.pack(fill=tk.X, pady=8) + ttk.Label(input_row, text="输入目录:", width=12, style="TLabel").pack(side=tk.LEFT, padx=5) + + # 创建可拖拽的输入框 + self.input_entry = ttk.Entry(input_row, textvariable=self.input_dir, width=60, style="TEntry") + self.input_entry.pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True) + + # 启用拖放功能 + self.input_entry.bind("", self.clear_placeholder) + self.input_entry.bind("", self.on_enter) + self.input_entry.bind("", self.on_leave) + + # 为输入框添加拖放支持 + self.input_entry.bind("", self.on_right_click) + + # 为窗口添加拖放支持(使用tkinter标准事件) + self.root.bind("", self.on_button_press) + self.root.bind("", self.on_mouse_move) + self.root.bind("", self.on_button_release) + + # 为输入框添加粘贴支持 + self.input_entry.bind("", self.on_paste) + self.input_entry.bind("", self.on_paste) + + ttk.Button(input_row, text="浏览", command=self.browse_input, style="Accent.TButton", width=8).pack(side=tk.RIGHT, padx=5) + + # 输出目录行 + output_row = ttk.Frame(input_frame) + output_row.pack(fill=tk.X, pady=8) + ttk.Label(output_row, text="输出目录:", width=12, style="TLabel").pack(side=tk.LEFT, padx=5) + ttk.Entry(output_row, textvariable=self.output_dir, width=60, style="TEntry").pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True) + ttk.Button(output_row, text="浏览", command=self.browse_output, style="Accent.TButton", width=8).pack(side=tk.RIGHT, padx=5) + + # 线程数和参数行 + settings_row = ttk.Frame(input_frame) + settings_row.pack(fill=tk.X, pady=8) + + threads_frame = ttk.Frame(settings_row) + threads_frame.pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True) + ttk.Label(threads_frame, text="并发线程数:", width=12, style="TLabel").pack(side=tk.LEFT, padx=5) + ttk.Entry(threads_frame, textvariable=self.threads, width=10, style="TEntry").pack(side=tk.LEFT, padx=5) + + # 分辨率设置按钮 + resolution_frame = ttk.LabelFrame(main_frame, text="分辨率设置", padding="15") + resolution_frame.pack(fill=tk.X, pady=10) + + buttons_frame = ttk.Frame(resolution_frame) + buttons_frame.pack(fill=tk.X, pady=5) + + ttk.Button(buttons_frame, text="强制高度为1080", command=self.set_height_1080, style="Accent.TButton").pack(side=tk.LEFT, padx=10, ipady=3, ipadx=15) + ttk.Button(buttons_frame, text="强制宽度为1920", command=self.set_width_1920, style="Accent.TButton").pack(side=tk.LEFT, padx=10, ipady=3, ipadx=15) + ttk.Button(buttons_frame, text="严格1080p(首选)", command=self.set_strict_1080p, style="Accent.TButton").pack(side=tk.LEFT, padx=10, ipady=3, ipadx=15) + + # FFmpeg参数设置 + params_frame = ttk.LabelFrame(main_frame, text="FFmpeg参数设置", padding="15") + params_frame.pack(fill=tk.X, pady=10) + + params_row = ttk.Frame(params_frame) + params_row.pack(fill=tk.X, pady=5) + ttk.Label(params_row, text="额外参数:", width=12, style="TLabel").pack(side=tk.LEFT, padx=5) + ttk.Entry(params_row, textvariable=self.ffmpeg_params, width=60, style="TEntry").pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True) + + # 控制按钮 + button_frame = ttk.Frame(main_frame) + button_frame.pack(fill=tk.X, pady=15) + + button_container = ttk.Frame(button_frame) + button_container.pack(side=tk.LEFT, padx=5) + + self.convert_button = ttk.Button(button_container, text="开始转换", command=self.start_conversion, style="Start.TButton") + self.convert_button.pack(side=tk.LEFT, padx=10, ipady=10, ipadx=25) + + self.stop_button = ttk.Button(button_container, text="停止转换", command=self.stop_conversion, state=tk.DISABLED, style="Stop.TButton") + self.stop_button.pack(side=tk.LEFT, padx=10, ipady=10, ipadx=25) + + # 状态框 + status_frame = ttk.LabelFrame(main_frame, text="转换状态", padding="15") + status_frame.pack(fill=tk.BOTH, expand=True, pady=10) + + # 状态文本框 + self.status_text = tk.Text(status_frame, wrap=tk.WORD, height=15, bg="#f8f8f8", fg=self.text_color, font=("Microsoft YaHei", 10)) + self.status_text.pack(fill=tk.BOTH, expand=True, side=tk.LEFT, padx=5, pady=5) + + # 滚动条 + scrollbar = ttk.Scrollbar(status_frame, command=self.status_text.yview, style="TScrollbar") + scrollbar.pack(side=tk.RIGHT, fill=tk.Y, pady=5) + self.status_text.config(yscrollcommand=scrollbar.set) + + # 底部状态 + self.status_var = tk.StringVar(value="就绪") + status_bar = ttk.Label(self.root, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W, style="Status.TLabel") + status_bar.pack(fill=tk.X, side=tk.BOTTOM) + + def setup_styles(self): + """设置界面样式""" + style = ttk.Style() + + # 主框架样式 + style.configure("Main.TFrame", background=self.bg_color) + + # 标题样式 + style.configure("Title.TLabel", font=("Microsoft YaHei", 18, "bold"), foreground=self.accent_color, background=self.bg_color) + + # 标签样式 + style.configure("TLabel", font=("Microsoft YaHei", 10, "bold"), foreground=self.text_color, background=self.frame_color) + + # 输入框样式 + style.configure("TEntry", font=("Microsoft YaHei", 10), padding=5, foreground=self.text_color) + # 悬停输入框样式 + style.configure("Hover.TEntry", font=("Microsoft YaHei", 10), padding=5, foreground=self.accent_color, background="#f0f8f0") + + # 按钮样式 + style.configure("Accent.TButton", font=("Microsoft YaHei", 10), padding=5, background=self.button_bg, foreground=self.button_fg) + style.map("Accent.TButton", background=[("active", "#45a049")]) + + # 开始按钮样式 + style.configure("Start.TButton", font=("Microsoft YaHei", 11, "bold"), padding=8, background=self.button_bg, foreground=self.button_fg) + style.map("Start.TButton", background=[("active", "#45a049")]) + + # 停止按钮样式 + style.configure("Stop.TButton", font=("Microsoft YaHei", 11, "bold"), padding=8, background="#f44336", foreground="#ffffff") + style.map("Stop.TButton", background=[("active", "#da190b")]) + + # 标签框架样式 + style.configure("TLabelFrame", font=("Microsoft YaHei", 10, "bold"), background=self.bg_color, foreground=self.text_color) + style.configure("TLabelFrame.Label", font=("Microsoft YaHei", 10, "bold"), foreground=self.text_color) + + # 滚动条样式 + style.configure("TScrollbar", background=self.bg_color, troughcolor="#e0e0e0") + + # 状态标签样式 + style.configure("Status.TLabel", font=("Microsoft YaHei", 9), padding=5, background="#e0e0e0", foreground=self.text_color) + + def browse_input(self): + dir_path = filedialog.askdirectory(title="选择输入目录") + if dir_path: + self.input_dir.set(dir_path) + + def browse_output(self): + dir_path = filedialog.askdirectory(title="选择输出目录") + if dir_path: + self.output_dir.set(dir_path) + + def set_height_1080(self): + """设置高度为1080""" + current_params = self.ffmpeg_params.get().strip() + # 移除现有的高度设置 + new_params = [] + for param in current_params.split(): + if not (param.startswith('-h') and param != '-h'): + new_params.append(param) + # 添加新的高度设置 + new_params.extend(['-vf', 'scale=-2:1080']) + self.ffmpeg_params.set(' '.join(new_params)) + self.log("已设置高度为1080") + + def set_width_1920(self): + """设置宽度为1920""" + current_params = self.ffmpeg_params.get().strip() + # 移除现有的宽度设置 + new_params = [] + for param in current_params.split(): + if not (param.startswith('-w') and param != '-w'): + new_params.append(param) + # 添加新的宽度设置 + new_params.extend(['-vf', 'scale=1920:-2']) + self.ffmpeg_params.set(' '.join(new_params)) + self.log("已设置宽度为1920") + + def set_strict_1080p(self): + """设置严格的1080p分辨率""" + current_params = self.ffmpeg_params.get().strip() + # 移除现有的分辨率设置 + new_params = [] + skip_next = False + for i, param in enumerate(current_params.split()): + if skip_next: + skip_next = False + continue + if param == '-vf' and i + 1 < len(current_params.split()): + if 'scale=' in current_params.split()[i + 1]: + skip_next = True + continue + new_params.append(param) + # 添加严格的1080p设置(先缩放保持比例,再用pad填充黑边至标准1080p) + new_params.extend(['-vf', 'scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2']) + self.ffmpeg_params.set(' '.join(new_params)) + self.log("已设置严格的1080p分辨率(保持比例+黑边填充)") + + def clear_placeholder(self, event): + """清除输入框的占位符""" + pass + + def on_enter(self, event): + """鼠标进入输入框""" + self.input_entry.configure(style="Hover.TEntry") + + def on_leave(self, event): + """鼠标离开输入框""" + self.input_entry.configure(style="TEntry") + + def on_button_press(self, event): + """鼠标按下""" + pass + + def on_mouse_move(self, event): + """鼠标移动""" + pass + + def on_button_release(self, event): + """鼠标释放""" + pass + + def on_right_click(self, event): + """右键点击""" + pass + + def on_paste(self, event): + """粘贴功能""" + try: + # 获取剪贴板内容 + clipboard_content = self.root.clipboard_get() + if clipboard_content: + # 检查是否是有效的文件或目录路径 + if os.path.exists(clipboard_content): + # 如果是文件,使用其所在目录 + if os.path.isfile(clipboard_content): + input_path = os.path.dirname(clipboard_content) + else: + input_path = clipboard_content + self.input_dir.set(input_path) + self.log(f"已添加输入目录: {input_path}") + except: + pass + + def log(self, message): + """记录日志到状态文本框""" + self.status_text.insert(tk.END, message + "\n") + self.status_text.see(tk.END) + self.root.update_idletasks() + + def get_video_files(self, directory): + """ + 获取目录下所有视频文件 + """ + video_extensions = ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv', '.webm', '.m4v'] + video_files = [] + + for root, _, files in os.walk(directory): + for file in files: + if any(file.lower().endswith(ext) for ext in video_extensions): + video_files.append(os.path.join(root, file)) + + return video_files + + def convert_video(self, input_file, output_file, ffmpeg_args): + """ + 转换单个视频文件 + """ + try: + # 构建ffmpeg命令 + cmd = [ + 'ffmpeg', '-i', input_file, + '-c:v', 'libx264', '-preset', 'medium', '-crf', '23', + '-c:a', 'aac', '-b:a', '128k', + '-y' # 覆盖输出文件 + ] + + # 检查用户是否设置了分辨率参数 + has_resolution_param = False + for i, arg in enumerate(ffmpeg_args): + if arg == '-vf' and i + 1 < len(ffmpeg_args): + if 'scale=' in ffmpeg_args[i + 1]: + has_resolution_param = True + break + + # 如果用户没有设置分辨率,使用默认的1080p横屏设置 + if not has_resolution_param: + cmd.extend(['-vf', 'scale=1920:1080,setdar=16:9']) # 默认强制横屏1080p + + # 添加用户自定义参数 + cmd.extend(ffmpeg_args) + cmd.append(output_file) + + # 执行命令 + self.log(f"正在转换: {os.path.basename(input_file)}") + subprocess.run(cmd, check=True, capture_output=True, text=True) + self.log(f"转换完成: {os.path.basename(output_file)}") + return True + except subprocess.CalledProcessError as e: + self.log(f"转换失败: {os.path.basename(input_file)}") + self.log(f"错误信息: {e.stderr}") + return False + except Exception as e: + self.log(f"转换失败: {os.path.basename(input_file)}") + self.log(f"错误信息: {str(e)}") + return False + + def start_conversion(self): + """开始转换过程""" + # 验证输入 + input_dir = self.input_dir.get().strip() + if not input_dir: + messagebox.showerror("错误", "请选择输入目录") + return + + if not os.path.exists(input_dir): + messagebox.showerror("错误", f"输入目录 '{input_dir}' 不存在") + return + + output_dir = self.output_dir.get().strip() + if not output_dir: + messagebox.showerror("错误", "请选择输出目录") + return + + try: + threads = int(self.threads.get().strip()) + if threads <= 0: + raise ValueError + except ValueError: + messagebox.showerror("错误", "并发线程数必须是正整数") + return + + # 解析FFmpeg参数 + ffmpeg_params = self.ffmpeg_params.get().strip() + ffmpeg_args = ffmpeg_params.split() if ffmpeg_params else [] + + # 更新UI状态 + self.is_converting = True + self.convert_button.config(state=tk.DISABLED) + self.stop_button.config(state=tk.NORMAL) + self.status_var.set("转换中...") + self.status_text.delete(1.0, tk.END) + + # 启动转换线程 + def conversion_thread(): + try: + # 创建输出目录 + os.makedirs(output_dir, exist_ok=True) + + # 获取所有视频文件 + video_files = self.get_video_files(input_dir) + + if not video_files: + self.log(f"错误: 输入目录 '{input_dir}' 中没有找到视频文件") + return + + self.log(f"找到 {len(video_files)} 个视频文件") + + # 并发转换视频 + success_count = 0 + failure_count = 0 + + with ThreadPoolExecutor(max_workers=threads) as executor: + future_to_video = {} + + for video_file in video_files: + if not self.is_converting: + break + + # 构建输出文件路径 + relative_path = os.path.relpath(video_file, input_dir) + output_file = os.path.join(output_dir, os.path.splitext(relative_path)[0] + '.mp4') + + # 创建输出文件的目录 + os.makedirs(os.path.dirname(output_file), exist_ok=True) + + # 提交任务 + future = executor.submit(self.convert_video, video_file, output_file, ffmpeg_args) + future_to_video[future] = video_file + + # 处理结果 + for future in as_completed(future_to_video): + if not self.is_converting: + # 取消所有未完成的任务 + for f in future_to_video: + if not f.done(): + f.cancel() + break + + video_file = future_to_video[future] + try: + result = future.result() + if result: + success_count += 1 + else: + failure_count += 1 + except Exception as e: + self.log(f"处理 {os.path.basename(video_file)} 时出错: {str(e)}") + failure_count += 1 + + if self.is_converting: + self.log(f"\n转换完成:") + self.log(f"成功: {success_count}") + self.log(f"失败: {failure_count}") + messagebox.showinfo("转换完成", f"转换完成:\n成功: {success_count}\n失败: {failure_count}") + else: + self.log("转换已停止") + messagebox.showinfo("转换停止", "转换已停止") + except Exception as e: + self.log(f"转换过程出错: {str(e)}") + messagebox.showerror("错误", f"转换过程出错: {str(e)}") + finally: + # 恢复UI状态 + self.is_converting = False + self.convert_button.config(state=tk.NORMAL) + self.stop_button.config(state=tk.DISABLED) + self.status_var.set("就绪") + + self.convert_thread = threading.Thread(target=conversion_thread) + self.convert_thread.daemon = True + self.convert_thread.start() + + def stop_conversion(self): + """停止转换过程""" + if messagebox.askyesno("确认", "确定要停止转换吗?"): + self.is_converting = False + self.status_var.set("停止中...") + + # 结束所有ffmpeg进程 + try: + import psutil + for proc in psutil.process_iter(['pid', 'name']): + if proc.info['name'] and 'ffmpeg' in proc.info['name'].lower(): + proc.kill() + self.log(f"已结束ffmpeg进程 (PID: {proc.info['pid']})") + except ImportError: + # 如果没有psutil,使用subprocess + try: + subprocess.run(['taskkill', '/F', '/IM', 'ffmpeg.exe'], capture_output=True) + self.log("已结束所有ffmpeg进程") + except: + self.log("结束ffmpeg进程失败,请手动结束") + except Exception as e: + self.log(f"结束ffmpeg进程时出错: {str(e)}") + + +def main(): + """ + 主函数 + """ + # 检查ffmpeg是否存在 + if not os.path.exists('ffmpeg.exe'): + # 尝试在系统路径中查找 + try: + subprocess.run(['ffmpeg', '-version'], capture_output=True, check=True) + except: + messagebox.showerror("错误", "未找到ffmpeg.exe,请确保它与程序在同一目录下") + return + + # 创建主窗口 + root = tk.Tk() + app = VideoConverterUI(root) + + # 运行主循环 + root.mainloop() + + +if __name__ == '__main__': + main()