吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 1590|回复: 28
上一主题 下一主题
收起左侧

[Python 原创] 视频管理小工具

  [复制链接]
跳转到指定楼层
楼主
wasxj100 发表于 2025-12-23 14:01 回帖奖励
本帖最后由 hrh123 于 2025-12-24 21:43 编辑

爱看电影的朋友们可以用此工具管理自己的电影啦,可以生成预览图奥。
看了大家的评论说不好下载,我又加了个度盘的分享链接,因为文件过大不能直接上传论坛服务器,所以只能这样。另外大家用的时候注意选择路径的话选外层的文件夹。
有小伙伴说生成预览速度一般,我研究研究改进后再发给大家。

"""
设置预览图有进度条版本
"""
import wx
import os
import cv2
import numpy as np
from PIL import Image
import random
from pathlib import Path
from moviepy import VideoFileClip

class MovieBrowserFrame(wx.Frame):
    def __init__(self, parent, title):
        super(MovieBrowserFrame, self).__init__(parent, title=title, size=(1000, 700))

        # 初始化变量
        self.base_folder = "" # 用户选择的根目录
        self.file_nodes = {} # 字典,key: 文件完整路径, value: 对应的TreeItemId (用于快速查找)
        self.selected_file_path = "" # 当前选中的文件路径
        # self.screenshotclass = ScreenshotClass(self.base_folder)
        # 创建主面板
        panel = wx.Panel(self)

        # 创建主布局 (水平分割)
        main_sizer = wx.BoxSizer(wx.HORIZONTAL)
        panel.SetSizer(main_sizer)

        # --- 左侧面板: 目录和文件树 ---
        left_panel = wx.Panel(panel)
        left_sizer = wx.BoxSizer(wx.VERTICAL)
        left_panel.SetSizer(left_sizer)

        # 创建一个水平容器放两个按钮
        button_sizer = wx.BoxSizer(wx.HORIZONTAL)

        # "选择目录" 按钮
        self.btn_select_folder = wx.Button(left_panel, label="选择根目录")
        self.btn_select_folder.Bind(wx.EVT_BUTTON, self.on_select_folder)
        button_sizer.Add(self.btn_select_folder, 0, wx.ALL | wx.EXPAND, 5)

        # 新增:"设置预览图" 按钮
        self.btn_set_preview_global = wx.Button(left_panel, label="设置预览图")
        self.btn_set_preview_global.Bind(wx.EVT_BUTTON, self.on_set_preview_global)
        self.btn_set_preview_global.Enable(False)  # 初始禁用,直到选择了根目录
        button_sizer.Add(self.btn_set_preview_global, 0, wx.ALL, 5)

        left_sizer.Add(button_sizer, 0, wx.EXPAND)

        # 树形控件 (展示目录和文件)
        # self.tree = wx.TreeCtrl(left_panel, style=wx.TR_DEFAULT_STYLE | wx.TR_HIDE_ROOT)
        self.tree = wx.TreeCtrl(left_panel, style=wx.TR_DEFAULT_STYLE)
        self.tree_root = self.tree.AddRoot("Loading...") # 隐藏根节点,但需要它来添加子节点
        self.tree.Bind(wx.EVT_TREE_ITEM_ACTIVATED, self.on_play_file) # 双击事件 (播放)
        self.tree.Bind(wx.EVT_TREE_SEL_CHANGED, self.on_tree_selection_changed) # 选中项改变事件
        left_sizer.Add(self.tree, 1, wx.ALL | wx.EXPAND, 5)

        # 将左侧面板加入主布局
        main_sizer.Add(left_panel, 1, wx.ALL | wx.EXPAND, 5)

        # --- 右侧面板: 预览图和信息 ---
        right_panel = wx.Panel(panel)
        right_sizer = wx.BoxSizer(wx.VERTICAL)
        right_panel.SetSizer(right_sizer)

        # 预览图标题
        self.lbl_preview_title = wx.StaticText(right_panel, label="电影预览")
        self.lbl_preview_title.SetFont(wx.Font(16, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD))
        right_sizer.Add(self.lbl_preview_title, 0, wx.ALL | wx.ALIGN_CENTER_HORIZONTAL, 5)

        # 预览图显示区域
        self.bitmap_preview = wx.StaticBitmap(right_panel, wx.ID_ANY, wx.NullBitmap, size=(300, 400))
        self.bitmap_preview.SetBackgroundColour(wx.Colour(200, 200, 200)) # 灰色背景
        right_sizer.Add(self.bitmap_preview, 0, wx.ALL | wx.ALIGN_CENTER_HORIZONTAL, 10)

        # 电影信息区域
        info_sizer = wx.FlexGridSizer(3, 2, 10, 10)
        info_sizer.AddGrowableCol(1, 1)

        info_sizer.Add(wx.StaticText(right_panel, label="文件名:"), 0, wx.ALIGN_CENTER_VERTICAL)
        self.lbl_filename = wx.StaticText(right_panel, label="")
        info_sizer.Add(self.lbl_filename, 0, wx.EXPAND)

        info_sizer.Add(wx.StaticText(right_panel, label="文件大小:"), 0, wx.ALIGN_CENTER_VERTICAL)
        self.lbl_filesize = wx.StaticText(right_panel, label="")
        info_sizer.Add(self.lbl_filesize, 0, wx.EXPAND)

        info_sizer.Add(wx.StaticText(right_panel, label="所属目录:"), 0, wx.ALIGN_CENTER_VERTICAL)
        self.lbl_directory = wx.StaticText(right_panel, label="")
        info_sizer.Add(self.lbl_directory, 0, wx.EXPAND)

        right_sizer.Add(info_sizer, 0, wx.ALL | wx.EXPAND, 10)

        # 播放按钮
        self.btn_play = wx.Button(right_panel, label="▶ 播放选中电影")
        self.btn_play.Bind(wx.EVT_BUTTON, self.on_play_file_button)
        self.btn_play.Enable(False) # 初始禁用
        right_sizer.Add(self.btn_play, 0, wx.ALL | wx.ALIGN_CENTER_HORIZONTAL, 5)

        # 将右侧面板加入主布局
        main_sizer.Add(right_panel, 1, wx.ALL | wx.EXPAND, 5)

        # 设置最小尺寸
        self.SetMinSize((800, 500))

        # 初始状态
        self.clear_preview()
        self.tree.CollapseAll() # 折叠所有

    def on_set_preview_global(self, event):
        """全局设置预览图:确认后调用 set_ppp"""
        if not self.base_folder or not Path(self.base_folder).is_dir():
            wx.MessageBox("请先选择一个有效的根目录。", "提示", wx.OK | wx.ICON_WARNING)
            return

        # 弹出确认对话框
        dlg = wx.MessageDialog(
            self,
            "确定要重新设置预览图吗?\n此操作将为所有视频生成或更新预览图。",
            "确认操作",
            wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION
        )
        result = dlg.ShowModal()
        dlg.Destroy()

        if result == wx.ID_YES:
            try:
                screenshot = ScreenshotClass(self.base_folder)
                video_files = screenshot.video_files
                if not video_files:
                    wx.MessageBox("根目录下未找到任何视频文件。", "提示", wx.OK | wx.ICON_INFORMATION)
                    return
                #创建进度对话框
                progress_dlg = wx.ProgressDialog(
                    "正在生成预览图...",
                    "准备中...",
                    maximum=len(video_files),
                    parent=self,
                    style=wx.PD_CAN_ABORT | wx.PD_APP_MODAL | wx.PD_ELAPSED_TIME | wx.PD_REMAINING_TIME
                )
                success_count = 0
                cancelled = False
                for i, video_path in enumerate(video_files):
                    # 更新进度对话框
                    msg = f"处理: {os.path.basename(video_path)}"
                    keep_going, skip = progress_dlg.Update(i, msg)
                    if not keep_going:
                        cancelled = True
                        break

                    try:
                        screenshot.screenshot(video_path)  # 你的截图逻辑
                        success_count += 1
                    except Exception as e:
                        print(f"处理 {video_path} 时出错: {e}")

                    # 让界面有机会响应(防止卡死)
                    wx.Yield()

                progress_dlg.Destroy()

                if not cancelled:
                    wx.MessageBox(f"✅ 预览图设置完成!\n成功处理 {success_count}/{len(video_files)} 个视频。", "成功",
                                  wx.OK | wx.ICON_INFORMATION)
                else:
                    wx.MessageBox("操作已取消。", "提示", wx.OK | wx.ICON_INFORMATION)

            except Exception as e:
                wx.MessageBox(f"设置预览图时出错:\n{str(e)}", "错误", wx.OK | wx.ICON_ERROR)

    def on_select_folder(self, event):
        dialog = wx.DirDialog(self, "选择电影根目录", style=wx.DD_DEFAULT_STYLE)
        if dialog.ShowModal() == wx.ID_OK:
            selected_path = dialog.GetPath()
            if os.path.isdir(selected_path):
                self.base_folder = selected_path
                self.build_tree()
                self.btn_set_preview_global.Enable(True)  # ← 启用新按钮
        dialog.Destroy()

    def build_tree(self):
        """构建目录和文件的树状结构:一级目录下显示其所有子目录中的视频文件"""
        self.tree.DeleteAllItems()
        self.file_nodes.clear()
        self.selected_file_path = ""
        self.clear_preview()

        if not self.base_folder or not os.path.exists(self.base_folder):
            self.SetTitle("电影浏览器")
            return

        # 创建根节点
        root_name = os.path.basename(self.base_folder)
        self.tree_root = self.tree.AddRoot(root_name)

        try:
            # 获取根目录下的所有条目
            entries = os.listdir(self.base_folder)
            # 过滤出子目录(一级目录)
            subdirs = [entry for entry in entries if os.path.isdir(os.path.join(self.base_folder, entry))]
            subdirs.sort()

            video_exts = ('.mp4', '.mov', '.avi', '.mkv', '.wmv', '.flv', '.webm', '.m4v')

            for subdir_name in subdirs:
                subdir_path = os.path.join(self.base_folder, subdir_name)
                # 在树中添加一级目录节点
                dir_node = self.tree.AppendItem(self.tree_root, subdir_name)

                # 🔥 使用 os.walk 递归遍历该一级目录下的所有子目录
                video_files_found = []  # 存储 (相对路径, 完整路径) 元组

                for current_dirpath, dirnames, filenames in os.walk(subdir_path):
                    for filename in filenames:
                        if filename.lower().endswith(video_exts):
                            file_path = os.path.join(current_dirpath, filename)
                            # 计算相对于一级目录的路径,用于显示
                            rel_path = os.path.relpath(current_dirpath, subdir_path)
                            if rel_path == '.':
                                display_path = filename
                            else:
                                display_path = f"{rel_path} / {filename}"  # 显示路径结构
                            video_files_found.append((display_path, file_path))

                # 按显示路径排序
                video_files_found.sort(key=lambda x: x[0])

                # 将所有找到的视频文件添加为 dir_node 的子节点
                for display_path, file_path in video_files_found:
                    # 使用 display_path 作为节点文本,展示文件路径
                    file_node = self.tree.AppendItem(dir_node, display_path)
                    self.file_nodes[file_path] = file_node  # 仍用完整路径映射

            # 展开根节点
            self.tree.Expand(self.tree_root)
            self.SetTitle(f"电影浏览器 - {root_name}")

        except Exception as e:
            wx.MessageBox(f"读取根目录时出错: {str(e)}", "错误", wx.OK | wx.ICON_ERROR)
            self.SetTitle("电影浏览器 - 读取错误")

    def on_tree_selection_changed(self, event):
        """处理树节点选中改变事件"""
        item = event.GetItem()
        if not item.IsOk():
            return

        item_text = self.tree.GetItemText(item)
        item_parent = self.tree.GetItemParent(item)

        # 只有当选中的是**文件节点**(即其父节点不是隐藏的根节点)时,才更新预览
        if item_parent and item_parent != self.tree_root: # 是文件节点
            file_path = None
            # 在file_nodes字典中查找
            for path, node in self.file_nodes.items():
                if node == item:
                    file_path = path
                    break

            if file_path and os.path.exists(file_path):
                self.selected_file_path = file_path
                self.update_file_info_and_preview(file_path)
                self.btn_play.Enable(True)
            else:
                self.clear_preview()
                self.btn_play.Enable(False)
        else:
            # 选中的是目录节点或根节点
            self.clear_preview()
            self.btn_play.Enable(False)

    def update_file_info_and_preview(self, file_path):
        """更新文件信息和预览图"""
        filename = os.path.basename(file_path)
        filesize = os.path.getsize(file_path)
        # 格式化文件大小
        if filesize < 1024:
            size_str = f"{filesize} B"
        elif filesize < 1024 * 1024:
            size_str = f"{filesize / 1024:.1f} KB"
        elif filesize < 1024 * 1024 * 1024:
            size_str = f"{filesize / (1024*1024):.1f} MB"
        else:
            size_str = f"{filesize / (1024*1024*1024):.1f} GB"

        # 所属目录
        dir_name = os.path.basename(os.path.dirname(file_path))

        self.lbl_filename.SetLabel(filename)
        self.lbl_filesize.SetLabel(size_str)
        self.lbl_directory.SetLabel(dir_name)

        # 加载预览图
        self.load_preview_image(file_path)

    def load_preview_image(self, file_path):
        """加载与电影同名的预览图"""
        base_name = os.path.splitext(file_path)[0]
        image_extensions = ['.jpg', '.jpeg', '.png', '.bmp', '.gif']
        preview_bitmap = None

        for ext in image_extensions:
            image_path = base_name + ext
            if os.path.exists(image_path):
                try:
                    image = wx.Image(image_path, wx.BITMAP_TYPE_ANY)
                    img_width, img_height = image.GetSize()
                    target_width, target_height = 300, 400
                    scale = min(target_width / img_width, target_height / img_height)
                    new_width = int(img_width * scale)
                    new_height = int(img_height * scale)
                    image = image.Scale(new_width, new_height, wx.IMAGE_QUALITY_HIGH)
                    preview_bitmap = wx.Bitmap(image)
                    break
                except Exception as e:
                    print(f"加载图片失败 {image_path}: {e}")
                    continue

        if preview_bitmap:
            self.bitmap_preview.SetBitmap(preview_bitmap)
        else:
            self.bitmap_preview.SetBitmap(wx.NullBitmap)

    def clear_preview(self):
        """清空预览区域"""
        self.selected_file_path = ""
        self.bitmap_preview.SetBitmap(wx.NullBitmap)
        self.lbl_filename.SetLabel("")
        self.lbl_filesize.SetLabel("")
        self.lbl_directory.SetLabel("")
        self.btn_play.Enable(False)

    def on_play_file(self, event=None):
        """播放选中的文件 (双击或按钮触发)"""
        if not self.selected_file_path or not os.path.exists(self.selected_file_path):
            wx.MessageBox("请先在列表中选择一个电影文件。", "提示", wx.OK | wx.ICON_INFORMATION)
            return

        try:
            wx.LaunchDefaultApplication(self.selected_file_path)
        except Exception as e:
            wx.MessageBox(f"无法播放文件: {str(e)}", "播放错误", wx.OK | wx.ICON_ERROR)

    def on_play_file_button(self, event):
        """播放按钮点击"""
        self.on_play_file()

class MovieBrowserApp(wx.App):
    def OnInit(self):
        frame = MovieBrowserFrame(None, "电影浏览器")
        frame.Show()
        return True

class ScreenshotClass:
    def __init__(self,path):
        self.path = path
        self.video_exts = ('.mp4', '.mov', '.avi', '.mkv', '.wmv')
        self.video_files = self.count_video()[0]
        self.count_video = self.count_video()[1]
    def count_video(self):
        if Path(self.path).is_dir():
            video_files = [f for f in Path(self.path).rglob("*")if f.is_file() and f.suffix.lower() in self.video_exts]
            return video_files, len(video_files)
        else:
            return [self.path],1

    def screenshot(self,video_file):
        try:
            self.snapshot_with_VFC(video_file)
        except Exception as e:
            print(f"发生错误: {e}")
            self.snapshot_with_opencv(video_file)

    def snapshot_with_opencv(self,video_path, num_frames=3,):
        path = Path(video_path)
        print(path)
        cap = cv2.VideoCapture(video_path)

        if not cap.isOpened():
            raise IOError(f"无法打开视频文件: {video_path}")

        # 获取总帧数和FPS
        total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
        if total_frames < num_frames:
            raise ValueError("视频太短,无法截取足够帧")

        # 随机选择帧索引
        selected_indices = sorted(random.sample(range(total_frames), num_frames))

        images = []
        for frame_idx in selected_indices:
            cap.set(cv2.CAP_PROP_POS_FRAMES, frame_idx)
            ret, frame = cap.read()
            if not ret:
                continue
            # BGR → RGB
            frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
            img = Image.fromarray(frame_rgb)
            images.append(img)

        cap.release()

        # 拼接图像(从上到下)
        widths, heights = zip(*(i.size for i in images))
        max_width = max(widths)
        total_height = sum(heights)

        concatenated = Image.new('RGB', (max_width, total_height))
        y_offset = 0
        for img in images:
            concatenated.paste(img, (0, y_offset))
            y_offset += img.height
        output_path = f"{path.parent/path.stem}.jpg"
        # print(output_path)
        concatenated.save(output_path)
        return output_path

    def snapshot_with_VFC(self,video_path):

        clip = VideoFileClip(video_path)

        # 获取视频时长(秒)
        duration = clip.duration
        print(duration)
        # 在 [0, duration] 范围内随机生成 3 个时间点(浮点数)
        times = sorted(np.random.uniform(0, duration, 3))

        # 获取截图(帧)
        frames = [clip.get_frame(t) for t in times]

        # 将 NumPy 数组(帧)转换为 PIL 图像
        images = [Image.fromarray(frame) for frame in frames]

        # 获取每张图像的尺寸
        widths, heights = zip(*(i.size for i in images))
        max_width = max(widths)
        total_height = sum(heights)

        # 创建一个空白图像用于垂直拼接
        concatenated_image = Image.new('RGB', (max_width, total_height))

        # 从上到下粘贴图像
        y_offset = 0
        for img in images:
            concatenated_image.paste(img, (0, y_offset))
            y_offset += img.height

        # 保存结果
        output_path = f"{video_path.parent/video_path.stem}.jpg"
        concatenated_image.save(output_path)

        # 释放视频资源
        clip.close()

        print(f"已保存拼接图像: {output_path}")
        print(f"截图时间点: {times}")

if __name__ == '__main__':
    app = MovieBrowserApp()
    app.MainLoop()

film_vx.txt

208 Bytes, 下载次数: 2, 下载积分: 吾爱币 -1 CB

下载链接

免费评分

参与人数 5吾爱币 +4 热心值 +4 收起 理由
亚马逊 + 1 还要登录,不能直接下载,能不能换个直接下载,下载方便
YYL7535 + 1 + 1 谢谢@Thanks!
wanfon + 1 + 1 热心回复!
wanc + 1 工具还需要持续打磨
朱古力 + 1 + 1 谢谢,试用了速度不算非常快,如果能生产GIF就无敌了

查看全部评分

本帖被以下淘专辑推荐:

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

推荐
朱古力 发表于 2025-12-23 17:39
谢谢,试用了速度不算非常快,弄了个20G的文件夹设置预览图,到最后几个卡死了。。。如果能生成GIF就无敌了
沙发
cn2jp 发表于 2025-12-23 15:42
3#
亚马逊 发表于 2025-12-23 15:50
4#
husay 发表于 2025-12-23 15:58
下载试试,支持原创!
5#
时间之源 发表于 2025-12-23 15:59
好软件先收藏了
6#
husay 发表于 2025-12-23 16:00
楼主,没共享到,不能下载啊?
7#
 楼主| wasxj100 发表于 2025-12-23 16:23 |楼主
husay 发表于 2025-12-23 16:00
楼主,没共享到,不能下载啊?

https://www.ilanzou.com/s/x7an6PUk
8#
 楼主| wasxj100 发表于 2025-12-23 16:24 |楼主
cn2jp 发表于 2025-12-23 15:42
任何视频格式都兼容吗?还是主流格式?

主流格式都支持
9#
a761199721 发表于 2025-12-23 16:26
我咋下载不了呢
10#
 楼主| wasxj100 发表于 2025-12-23 16:32 |楼主

翻翻楼层,我发了下载链接
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2025-12-25 14:01

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表