325 lines
14 KiB
Python
325 lines
14 KiB
Python
|
|
import os
|
|||
|
|
import sys
|
|||
|
|
import subprocess
|
|||
|
|
import tkinter as tk
|
|||
|
|
from tkinter import messagebox
|
|||
|
|
from datetime import datetime
|
|||
|
|
from send2trash import send2trash # 需提前安装:pip install send2trash
|
|||
|
|
|
|||
|
|
# =================================================================
|
|||
|
|
# 请在这里修改为您的实际路径!
|
|||
|
|
# =================================================================
|
|||
|
|
# F 盘:视频文件和此Python脚本所在的目录(自动获取,无需手动改)
|
|||
|
|
F_DISK_VIDEO_DIR = os.path.dirname(os.path.abspath(__file__))
|
|||
|
|
|
|||
|
|
# E 盘:与视频同名的文件夹所在的根目录(当前设为C盘根目录,需根据实际改)
|
|||
|
|
E_DISK_FOLDERS_ROOT_DIR = "E:\\"
|
|||
|
|
|
|||
|
|
# 日志文件配置(保存在F盘,与脚本同级)
|
|||
|
|
PASS_LOG_FILE = "passed_files.txt" # 通过视频日志
|
|||
|
|
FAIL_LOG_FILE = "rejected_files.txt" # 不通过视频日志
|
|||
|
|
# 新增视频检测间隔(秒)
|
|||
|
|
DETECTION_INTERVAL = 5 # 每5秒检测一次新视频
|
|||
|
|
# =================================================================
|
|||
|
|
|
|||
|
|
class VideoProcessorGUI:
|
|||
|
|
def __init__(self, root):
|
|||
|
|
"""初始化GUI窗口和变量"""
|
|||
|
|
self.root = root
|
|||
|
|
self.root.title("视频批量标记工具 (支持实时新增视频)")
|
|||
|
|
self.root.geometry("550x220")
|
|||
|
|
self.root.resizable(False, False)
|
|||
|
|
|
|||
|
|
# 检查E盘(当前是C盘)文件夹根目录是否存在
|
|||
|
|
if not os.path.isdir(E_DISK_FOLDERS_ROOT_DIR):
|
|||
|
|
messagebox.showerror("错误", f"目标盘文件夹根目录不存在!\n请检查路径: {E_DISK_FOLDERS_ROOT_DIR}")
|
|||
|
|
self.root.destroy()
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
# 初始化视频文件列表(存储完整路径)
|
|||
|
|
self.video_files = self._get_video_files()
|
|||
|
|
self.current_index = 0
|
|||
|
|
self.last_checked_time = datetime.now() # 记录最后一次检测时间
|
|||
|
|
|
|||
|
|
# 创建界面组件
|
|||
|
|
self._create_widgets()
|
|||
|
|
# 开始处理第一个视频
|
|||
|
|
self._process_current_video()
|
|||
|
|
# 启动新视频检测定时器
|
|||
|
|
self._start_detection_timer()
|
|||
|
|
|
|||
|
|
def _force_focus_timer(self):
|
|||
|
|
"""定时强制获取焦点"""
|
|||
|
|
self.root.focus_force() # 强制获取焦点
|
|||
|
|
self.root.after(500, self._force_focus_timer)
|
|||
|
|
|
|||
|
|
def _get_video_files(self):
|
|||
|
|
"""获取F盘当前目录下的所有视频文件(仅返回完整路径)"""
|
|||
|
|
video_extensions = ('.mp4', '.mkv', '.avi', '.mov', '.flv', '.wmv', '.webm', '.ts')
|
|||
|
|
video_files = [
|
|||
|
|
os.path.join(F_DISK_VIDEO_DIR, f)
|
|||
|
|
for f in os.listdir(F_DISK_VIDEO_DIR)
|
|||
|
|
if os.path.isfile(os.path.join(F_DISK_VIDEO_DIR, f)) and f.lower().endswith(video_extensions)
|
|||
|
|
]
|
|||
|
|
return video_files # 不排序,确保新增文件按系统顺序添加
|
|||
|
|
|
|||
|
|
def _create_widgets(self):
|
|||
|
|
"""创建弹窗中的按钮、文字等组件"""
|
|||
|
|
# 新视频提示标签(红色提示新增文件)
|
|||
|
|
self.new_files_label = tk.Label(self.root, text="", fg="red", pady=5)
|
|||
|
|
self.new_files_label.pack()
|
|||
|
|
|
|||
|
|
# 状态提示(如“正在处理第1/5个”)
|
|||
|
|
self.status_label = tk.Label(self.root, text="", pady=5)
|
|||
|
|
self.status_label.pack()
|
|||
|
|
|
|||
|
|
# 当前视频文件名(蓝色加粗)
|
|||
|
|
self.filename_label = tk.Label(self.root, text="", font=("Arial", 12), fg="blue")
|
|||
|
|
self.filename_label.pack()
|
|||
|
|
|
|||
|
|
# 按钮容器(让按钮横向排列)
|
|||
|
|
button_frame = tk.Frame(self.root)
|
|||
|
|
button_frame.pack(side=tk.BOTTOM, pady=20)
|
|||
|
|
|
|||
|
|
self.root.bind('<Key>', self._handle_key_press)
|
|||
|
|
self.root.focus_force() # 强制获取焦点
|
|||
|
|
self.root.bind('<FocusOut>', lambda e: self.root.focus_force())
|
|||
|
|
|
|||
|
|
# 上一条/重播/通过/不通过/下一条按钮
|
|||
|
|
self.prev_btn = tk.Button(button_frame, text="上一条", width=10, command=self._prev_video)
|
|||
|
|
self.replay_btn = tk.Button(button_frame, text="重播", width=10, command=self._replay_video)
|
|||
|
|
self.pass_btn = tk.Button(button_frame, text="通过", width=10, command=self._mark_as_pass, bg="green", fg="white")
|
|||
|
|
self.fail_btn = tk.Button(button_frame, text="不通过", width=10, command=self._mark_as_fail, bg="red", fg="white")
|
|||
|
|
self.next_btn = tk.Button(button_frame, text="下一条", width=10, command=self._next_video)
|
|||
|
|
|
|||
|
|
# 按钮布局(横向均匀分布)
|
|||
|
|
self.prev_btn.pack(side=tk.LEFT, padx=5)
|
|||
|
|
self.replay_btn.pack(side=tk.LEFT, padx=5)
|
|||
|
|
self.pass_btn.pack(side=tk.LEFT, padx=5)
|
|||
|
|
self.fail_btn.pack(side=tk.LEFT, padx=5)
|
|||
|
|
self.next_btn.pack(side=tk.LEFT, padx=5)
|
|||
|
|
|
|||
|
|
def _start_detection_timer(self):
|
|||
|
|
"""启动定时器,定期检测新视频文件"""
|
|||
|
|
self._check_new_videos() # 立即执行一次检测
|
|||
|
|
# 每隔DETECTION_INTERVAL秒重复检测
|
|||
|
|
self.root.after(DETECTION_INTERVAL * 1000, self._start_detection_timer)
|
|||
|
|
|
|||
|
|
def _check_new_videos(self):
|
|||
|
|
"""检测并添加新增的视频文件,若之前无视频则自动启动播放"""
|
|||
|
|
if not os.path.isdir(F_DISK_VIDEO_DIR):
|
|||
|
|
return # 目录不存在则跳过检测
|
|||
|
|
|
|||
|
|
# 获取当前目录下的所有视频(最新状态)
|
|||
|
|
current_all_videos = self._get_video_files()
|
|||
|
|
# 找出不在当前处理列表中的新视频(通过路径对比)
|
|||
|
|
existing_paths = set(self.video_files)
|
|||
|
|
new_videos = [path for path in current_all_videos if path not in existing_paths]
|
|||
|
|
|
|||
|
|
if new_videos:
|
|||
|
|
# 记录新增前的列表长度(判断是否之前无视频)
|
|||
|
|
was_empty = len(self.video_files) == 0
|
|||
|
|
|
|||
|
|
# 将新视频添加到处理列表末尾
|
|||
|
|
self.video_files.extend(new_videos)
|
|||
|
|
# 提取新视频的文件名(用于提示)
|
|||
|
|
new_filenames = [os.path.basename(path) for path in new_videos]
|
|||
|
|
# 显示新增提示(3秒后自动清除提示)
|
|||
|
|
self.new_files_label.config(text=f"检测到 {len(new_videos)} 个新视频,已添加到列表:{', '.join(new_filenames[:2])}{'...' if len(new_filenames)>2 else ''}")
|
|||
|
|
self.root.after(3000, lambda: self.new_files_label.config(text="")) # 3秒后清除提示
|
|||
|
|
print(f"新增视频:{new_filenames}")
|
|||
|
|
|
|||
|
|
# 核心修复:若之前无视频,新增后自动从第一个新视频开始播放
|
|||
|
|
if was_empty:
|
|||
|
|
self.current_index = 0 # 确保从第一个新增视频开始
|
|||
|
|
self._process_current_video() # 触发播放
|
|||
|
|
|
|||
|
|
# 更新最后检测时间
|
|||
|
|
self.last_checked_time = datetime.now()
|
|||
|
|
|
|||
|
|
def _play_video(self, video_path):
|
|||
|
|
"""用系统默认播放器打开视频"""
|
|||
|
|
try:
|
|||
|
|
if sys.platform == 'win32': # 针对Windows系统
|
|||
|
|
os.startfile(video_path)
|
|||
|
|
else: # 针对Mac/Linux系统(备用)
|
|||
|
|
opener = 'open' if sys.platform == 'darwin' else 'xdg-open'
|
|||
|
|
subprocess.run([opener, video_path], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|||
|
|
except Exception as e:
|
|||
|
|
# 播放失败时弹窗提示
|
|||
|
|
error_msg = (
|
|||
|
|
f"无法播放视频!\n\n"
|
|||
|
|
f"系统错误信息: {e}\n\n"
|
|||
|
|
f"**请检查:**\n"
|
|||
|
|
f"1. 您是否已安装视频播放器?\n"
|
|||
|
|
f"2. 您是否已将其设置为默认播放器?"
|
|||
|
|
)
|
|||
|
|
messagebox.showerror("播放错误", error_msg)
|
|||
|
|
|
|||
|
|
def _log_file(self, filename, log_type):
|
|||
|
|
"""通用日志记录方法:根据log_type记录到对应的TXT文件"""
|
|||
|
|
try:
|
|||
|
|
# 选择日志文件(通过→passed_files.txt,不通过→rejected_files.txt)
|
|||
|
|
log_file = PASS_LOG_FILE if log_type == "pass" else FAIL_LOG_FILE
|
|||
|
|
# 日志文件路径:和脚本、视频在同一目录(F盘)
|
|||
|
|
log_file_path = os.path.join(F_DISK_VIDEO_DIR, log_file)
|
|||
|
|
# 追加写入(不覆盖历史记录),包含时间戳
|
|||
|
|
with open(log_file_path, 'a', encoding='utf-8') as f:
|
|||
|
|
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|||
|
|
f.write(f"[{timestamp}] {filename}\n")
|
|||
|
|
return True # 记录成功
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"写入日志失败: {e}") # 终端打印错误(不影响用户操作)
|
|||
|
|
return False # 记录失败
|
|||
|
|
|
|||
|
|
def _handle_key_press(self, event):
|
|||
|
|
"""处理键盘按键"""
|
|||
|
|
if event.char == '2': # 按下1键
|
|||
|
|
self._mark_as_pass()
|
|||
|
|
elif event.char == '3': # 按下2键
|
|||
|
|
self._mark_as_fail()
|
|||
|
|
|
|||
|
|
def _process_current_video(self):
|
|||
|
|
"""更新弹窗状态,并自动播放当前视频"""
|
|||
|
|
if not self.video_files: # 没有视频可处理时
|
|||
|
|
self.status_label.config(text="所有视频处理完毕!(将继续检测新视频...)", fg="green")
|
|||
|
|
self.filename_label.config(text="")
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
# 获取当前视频路径和文件名
|
|||
|
|
current_video_path = self.video_files[self.current_index]
|
|||
|
|
current_video_name = os.path.basename(current_video_path)
|
|||
|
|
|
|||
|
|
# 更新弹窗文字提示(覆盖之前的“处理完毕”提示)
|
|||
|
|
self.status_label.config(text=f"正在处理: 第 {self.current_index + 1}/{len(self.video_files)} 个")
|
|||
|
|
self.filename_label.config(text=current_video_name)
|
|||
|
|
|
|||
|
|
# 自动播放当前视频
|
|||
|
|
self._play_video(current_video_path)
|
|||
|
|
|
|||
|
|
def _get_associated_folder(self, video_path):
|
|||
|
|
"""获取E盘(当前是C盘)上与视频同名的文件夹路径(仅查询,不操作)"""
|
|||
|
|
video_basename = os.path.basename(video_path)
|
|||
|
|
folder_name = os.path.splitext(video_basename)[0] # 提取视频名(去后缀)
|
|||
|
|
folder_path = os.path.join(E_DISK_FOLDERS_ROOT_DIR, folder_name)
|
|||
|
|
|
|||
|
|
if os.path.isdir(folder_path):
|
|||
|
|
return folder_path
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
def _mark_as_pass(self):
|
|||
|
|
"""处理“通过”:删除视频+记录到passed_files.txt"""
|
|||
|
|
if not self.video_files:
|
|||
|
|
return
|
|||
|
|
os.system('taskkill /f /im "vlc.exe"')
|
|||
|
|
# 获取当前视频信息(路径、带后缀名、去后缀名)
|
|||
|
|
current_video_path = self.video_files[self.current_index]
|
|||
|
|
video_basename = os.path.basename(current_video_path)
|
|||
|
|
video_filename = os.path.splitext(video_basename)[0] # 去后缀的文件名(如“视频1”)
|
|||
|
|
|
|||
|
|
|
|||
|
|
# 1. 记录“通过”日志
|
|||
|
|
log_success = self._log_file(video_filename, "pass")
|
|||
|
|
|
|||
|
|
# 2. 提示用户操作结果(包含日志记录情况)
|
|||
|
|
if log_success:
|
|||
|
|
messagebox.showinfo("操作结果",
|
|||
|
|
f"已标记为通过: {video_filename}\n"
|
|||
|
|
f"文件名已记录到 {PASS_LOG_FILE}")
|
|||
|
|
else:
|
|||
|
|
messagebox.showerror("操作结果",
|
|||
|
|
f"已标记为通过: {video_filename}\n"
|
|||
|
|
f"警告:文件名未成功记录到 {PASS_LOG_FILE}")
|
|||
|
|
|
|||
|
|
# 3. 删除F盘的视频(移至回收站,安全删除)
|
|||
|
|
try:
|
|||
|
|
send2trash(current_video_path)
|
|||
|
|
print(f"通过视频已删除: {current_video_path}")
|
|||
|
|
except Exception as e:
|
|||
|
|
messagebox.showerror("操作失败",
|
|||
|
|
f"无法删除视频: {e}\n"
|
|||
|
|
f"请确保视频没有被播放器占用(关闭播放器后重试)")
|
|||
|
|
|
|||
|
|
# 4. 处理下一个视频
|
|||
|
|
self._move_to_next()
|
|||
|
|
|
|||
|
|
def _mark_as_fail(self):
|
|||
|
|
"""处理“不通过”:删除视频+记录到rejected_files.txt"""
|
|||
|
|
if not self.video_files:
|
|||
|
|
return
|
|||
|
|
os.system('taskkill /f /im "vlc.exe"')
|
|||
|
|
# 获取当前视频信息
|
|||
|
|
current_video_path = self.video_files[self.current_index]
|
|||
|
|
video_basename = os.path.basename(current_video_path)
|
|||
|
|
video_filename = os.path.splitext(video_basename)[0]
|
|||
|
|
|
|||
|
|
|
|||
|
|
# 1. 记录“不通过”日志
|
|||
|
|
log_success = self._log_file(video_filename, "fail")
|
|||
|
|
|
|||
|
|
# 2. 提示用户操作结果
|
|||
|
|
if log_success:
|
|||
|
|
messagebox.showinfo("操作结果",
|
|||
|
|
f"已标记为不通过: {video_filename}\n"
|
|||
|
|
f"文件名已记录到 {FAIL_LOG_FILE}")
|
|||
|
|
else:
|
|||
|
|
messagebox.showerror("操作结果",
|
|||
|
|
f"已标记为不通过: {video_filename}\n"
|
|||
|
|
f"警告:文件名未成功记录到 {FAIL_LOG_FILE}")
|
|||
|
|
|
|||
|
|
# 3. 删除F盘的视频(移至回收站)
|
|||
|
|
try:
|
|||
|
|
send2trash(current_video_path)
|
|||
|
|
print(f"不通过视频已删除: {current_video_path}")
|
|||
|
|
except Exception as e:
|
|||
|
|
messagebox.showerror("操作失败",
|
|||
|
|
f"无法删除视频: {e}\n"
|
|||
|
|
f"请确保视频没有被播放器占用(关闭播放器后重试)")
|
|||
|
|
|
|||
|
|
# 4. 处理下一个视频
|
|||
|
|
self._move_to_next()
|
|||
|
|
|
|||
|
|
def _move_to_next(self):
|
|||
|
|
"""移除已处理的视频,切换到下一个"""
|
|||
|
|
if not self.video_files:
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
self.video_files.pop(self.current_index)
|
|||
|
|
# 避免删除最后一个视频后索引越界
|
|||
|
|
if self.current_index >= len(self.video_files) and self.video_files:
|
|||
|
|
self.current_index = len(self.video_files) - 1
|
|||
|
|
# 更新弹窗状态,播放下一个视频
|
|||
|
|
self._process_current_video()
|
|||
|
|
|
|||
|
|
def _replay_video(self):
|
|||
|
|
"""重播当前视频"""
|
|||
|
|
if self.video_files:
|
|||
|
|
self._play_video(self.video_files[self.current_index])
|
|||
|
|
|
|||
|
|
def _next_video(self):
|
|||
|
|
"""切换到下一个视频(不处理当前视频)"""
|
|||
|
|
if not self.video_files:
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
if self.current_index < len(self.video_files) - 1:
|
|||
|
|
self.current_index += 1
|
|||
|
|
self._process_current_video()
|
|||
|
|
else:
|
|||
|
|
messagebox.showinfo("提示", "已经是最后一个视频了。")
|
|||
|
|
|
|||
|
|
def _prev_video(self):
|
|||
|
|
"""切换到上一个视频(不处理当前视频)"""
|
|||
|
|
if not self.video_files:
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
if self.current_index > 0:
|
|||
|
|
self.current_index -= 1
|
|||
|
|
self._process_current_video()
|
|||
|
|
else:
|
|||
|
|
messagebox.showinfo("提示", "已经是第一个视频了。")
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
# 启动弹窗程序
|
|||
|
|
root = tk.Tk()
|
|||
|
|
app = VideoProcessorGUI(root)
|
|||
|
|
root.mainloop()
|