python-script-library/videos质检程序.py

325 lines
14 KiB
Python
Raw Normal View History

2025-10-28 16:02:55 +08:00
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()