今天分享一个 Python 脚本,这个 Python脚本借助moviepy和FFmpeg,将指定的视频文档转码为mp3文档。
本Python脚本借助Everything的强大搜索能力,在协助用户搜索和定位视频文件方面提供了极大的便利。
本Python脚本默认将转码的文件与源视频文件存放于相同中路径中,如果遇到写入权限不足的情况,会提示用户另行指定其它存放路径。
本Python脚本保存转码文件时,将根据用户指定的文档格式,在源文件名的基础上追加新的后缀名作为新的文件名,例如:<源视频文档名>.用户指定的新后缀名
本Python脚本在进行视频文档转码时,提供了默认了编解码器,同时亦允许用户指定新的编解码器。
Python 脚本
Python脚本比较简单,逻辑也比较清晰。截图如下:
如👆上图所示,脚本首先定义了一个命令行入参的类,用于管理和解析命令行传入的参数。
然后定义了两个小的函数,这两个函数并不是核心功能,仅用于对比两个指定的字符串中的差异,以及协助在需要时调用TotalCommander打开指定的路径(如果电脑上不存在在TotalCommander,则会使用默认的资源管理器打开指定路径)。
在主程序中(if __name__ == '__main__'
部分),脚本首先解析了来自命令行的参数;
然后定义了一个Everything搜索接口对象(该Everything搜索接口对象提供了基于Everything的搜索功能),用于协助用户快速的搜索定位兴趣文档。
关于如何配置Everything的http服务,相关的教程资料比较多,大家可以自行搜索哈。
在功能逻辑循环体中,首先引导用户搜索定位待转码的视频文档;然后与用户交互确定转码的目标格式;最后借助 moviepy这个Python包完成视频转码操作;转码操作完成后,脚本会询问是否要打开目标文档所在的路径,脚本可以为用户打开这些目标文档的路径以供用户进行后续的操作。
以上,脚本逻辑清晰自然,贴出Python代码如下👇。
# -*- coding:UTF-8 -*-
"""
@author: dyy
@contact: douyaoyuan@126.com
@time: 2024/5/10 21:40
@file: 视频转码.py
@desc: xxxxxx
"""
# region 引入必要的依赖
import os
import subprocess
模块名 = 'DebugInfo'
try:
from DebugInfo.DebugInfo import *
except ImportError as impErr:
print(f"尝试导入 {模块名} 依赖时检测到异常:{impErr}")
print(f"尝试安装 {模块名} 模块:")
try:
os.system(f"pip install {模块名}")
except OSError as osErr:
print(f"尝试安装模块 {模块名} 时检测到异常:{osErr}")
exit(0)
else:
try:
from DebugInfo.DebugInfo import *
except ImportError as impErr:
print(f"再次尝试导入 {模块名} 依赖时检测到异常:{impErr}")
exit(0)
模块名 = 'difflib'
try:
import difflib # 需要安装 difflib 模块,以支持字符差异对比操作
except ImportError as impErr:
print(f"尝试导入 {模块名} 依赖时检测到异常:{impErr}")
print(f"尝试安装 {模块名} 模块:")
try:
os.system(f"pip install {模块名}")
except OSError as osErr:
print(f"尝试安装模块 {模块名} 时检测到异常:{osErr}")
exit(0)
else:
try:
import difflib
except ImportError as impErr:
print(f"再次尝试导入 {模块名} 依赖时检测到异常:{impErr}")
exit(0)
模块名 = 'moviepy'
try:
from moviepy.editor import VideoFileClip # 需要引入 moviepy 模块,以支持视频转码的操作
except ImportError as impErr:
print(f"尝试导入 {模块名} 依赖时检测到异常:{impErr}")
print(f"尝试安装 {模块名} 模块:")
try:
os.system(f"pip install {模块名}")
except OSError as osErr:
print(f"尝试安装模块 {模块名} 时检测到异常:{osErr}")
exit(0)
else:
try:
from moviepy.editor import VideoFileClip
except ImportError as impErr:
print(f"再次尝试导入 {模块名} 依赖时检测到异常:{impErr}")
exit(0)
# endregion
# 定义一个 命令行参数类,用于解析和记录命令行参数
class 命令行参数类(入参基类):
def __init__(self):
super().__init__()
self._添加参数('搜索关键字', str, '指定文档搜索的关键字')
self._添加参数('everything服务端口', str, 'everything HTTP 服务端口', '22')
# region 访问器
@property
def jsonCfg(self) -> str:
if 'jsonCfg' in self._参数字典:
return self._参数字典['jsonCfg'].值
else:
return ''
@jsonCfg.setter
def jsonCfg(self, 值: str):
if 'jsonCfg' in self._参数字典:
self._参数字典['jsonCfg'].值 = str(值)
@property
def 搜索关键字(self) -> str:
if '搜索关键字' in self._参数字典:
return self._参数字典['搜索关键字'].值
else:
return ''
@搜索关键字.setter
def 搜索关键字(self, 值: str):
if '搜索关键字' in self._参数字典:
self._参数字典['搜索关键字'].值 = str(值)
@property
def everything服务端口(self) -> str:
if 'everything服务端口' in self._参数字典:
return self._参数字典['everything服务端口'].值
else:
return ''
@everything服务端口.setter
def everything服务端口(self, 值: str):
if 'everything服务端口' in self._参数字典:
self._参数字典['everything服务端口'].值 = str(值)
# endregion
# 标注两个字符串的差异项
def 差异标注(字串1: str, 字串2: str) -> tuple[str, str]:
字串1 = str(字串1 if 字串1 else '').strip()
字串2 = str(字串2 if 字串2 else '').strip()
# region 原文档变动对齐
# 使用SequenceMatcher()类计算两个字符串的相似度
匹配器 = difflib.SequenceMatcher(None, 字串1, 字串2)
差异 = 匹配器.get_opcodes()
# 差异标注
字串1差异标注: str = ''
字串2差异标注: str = ''
for opcode, i1, i2, j1, j2 in 差异:
if opcode == 'equal':
字串1差异标注 = 字串1差异标注 + 字串1[i1:i2]
字串2差异标注 = 字串2差异标注 + 字串2[j1:j2]
elif opcode == 'delete':
字串1差异标注 = 字串1差异标注 + 红字(字串1[i1:i2])
elif opcode == 'insert':
字串2差异标注 = 字串2差异标注 + 绿字(字串2[j1:j2])
elif opcode == 'replace':
字串1差异标注 = 字串1差异标注 + 红字(字串1[i1:i2])
字串2差异标注 = 字串2差异标注 + 绿字(字串2[j1:j2])
return 字串1差异标注, 字串2差异标注
def 通过totalCommander打开指定路径(路径: str, 搜索接口: 搜索接口类 = None, 画板: 打印模板 = False):
画板 = 画板 if isinstance(画板, 打印模板) else 打印模板()
画板.执行位置(通过totalCommander打开指定路径)
路径 = str(路径 if 路径 else '').strip()
if not os.path.isdir(路径):
if os.path.isfile(路径):
路径 = os.path.abspath(路径)
else:
return None
TOTALCMD64 = os.getenv('TOTALCMD64')
if not TOTALCMD64:
# 如果 TOTALCMD64 变量不存在,则尝试搜索 TOTALCMD64.exe 程序
if isinstance(搜索接口, 搜索接口类) and 搜索接口.可用:
搜索结果 = 搜索接口.搜索(搜索关键字='TOTALCMD64.EXE',
搜文档=True,
搜路径=False,
画板=画板.副本)
if 搜索结果.总数 > 0:
for 结果 in 搜索结果.结果列表:
if 结果.lower().endswith('totalcmd64.exe'):
TOTALCMD64 = 结果
break
if TOTALCMD64 and TOTALCMD64.lower().endswith('totalcmd64.exe') and os.path.isfile(TOTALCMD64):
# 尝试使用 totalcommander 来打开路径
shell指令: str = f'"{TOTALCMD64}" /O /T /R="{路径}"'
subprocess.Popen(shell指令)
else:
# 使用系统默认程序打开路径
if 在nt系统中():
os.startfile(路径)
else:
画板.提示错误('不受支持的操作系统')
if __name__ == '__main__':
画板 = 打印模板(False)
画板.执行位置(__file__)
# region 解析入参
入参 = 命令行参数类()
入参.解析Json(画板=画板.副本.缩进())
入参.解析入参(画板=画板.副本.缩进())
if 画板.正在调试:
入参.展示(画板=画板.副本.缩进())
搜索关键字: str = 入参.搜索关键字 if 入参.搜索关键字 else ''
# endregion
# region 定义搜索接口
本地搜索: 搜索接口类 = 搜索接口类()
if 在nt系统中():
if 入参.everything服务端口:
本地搜索.everything服务端口 = int(入参.everything服务端口)
else:
画板.提示错误('入参.everything服务端口 参数无效')
exit(1)
# endregion
已经打开的目标视频文档路径: list[str] = []
# region 开始逻辑循环
while True:
def 文档排除规则(文档: str) -> bool: # 排除一些不必要的干扰文档
if not 文档: # 空文档不要
return True
if '\\recent\\' in 文档.lower(): # recent 文档夹内的文档不要
return True
if 文档.lower().endswith('.lnk'): # *.lnk 文档不要
return True
# region 引导用户选择需要转码的视频文档
while True:
原视频文档列表 = 交互接口类.指定选择文档(输入提示='请输入关键字以定位视频文档(0: 退出程序):',
搜索接口=本地搜索,
搜索关键字=搜索关键字,
候选项上限=-1,
排除规则=文档排除规则,
多选=True,
画板=画板.副本.缩进())
if '0' in 原视频文档列表:
# 用户要求退出程序
exit(0)
if 原视频文档列表:
画板.消息('您选择的文档列出如下:')
for 文档 in 原视频文档列表:
画板.消息(绿字(文档))
break
# endregion
目标视频文档列表: list[str]
目标视频文档存在性列表: list[bool]
# region 目标格式信息交互
while True:
目标视频文档列表 = []
目标视频文档存在性列表 = []
目标格式后缀名: str = 交互接口类.发起文本交互(输入提示='请输入视频目标格式(0: 退出程序):',
画板=画板.副本.缩进())
if '0' == 目标格式后缀名:
# 用户要求退出程序
exit(0)
if 目标格式后缀名.startswith('。'):
目标格式后缀名 = f'.{目标格式后缀名[1:]}'
if not 目标格式后缀名.startswith('.'):
目标格式后缀名 = f'.{目标格式后缀名}'
if not 目标格式后缀名.strip('.'):
# 如果后缀名是空的,则重新要求输入
continue
# 后缀名统一转换成小写格式
目标格式后缀名 = 目标格式后缀名.lower()
# 合成目标文档名:
for 文档 in 原视频文档列表:
目标视频文档列表.append(f'{文档}{目标格式后缀名}')
目标视频文档存在性列表.append(os.path.isfile(f'{文档}{目标格式后缀名}'))
# 打印并提示用户确认转换方式
画板.消息(黄字('即将启动的转码过程如下:'))
画板.准备表格(对齐控制串='rl')
for 序号 in range(len(原视频文档列表)):
原文档,目标文档 = 差异标注(原视频文档列表[序号],目标视频文档列表[序号])
画板.添加一行(原文档, 洋红字('->'), 目标文档, 红字('已存在') if 目标视频文档存在性列表[序号] else 绿字('待转码'))
画板.展示表格(列间距=[0,0])
转码确认: str = 交互接口类.发起文本交互(输入提示=f'是否确认转码?({绿字("y: 确认")}; {黄字("m: 修改目标格式")}; {红字("0: 退出程序")})',
限定范围='YyMm0',
画板=画板.副本.缩进()).lower()
if '0' == 转码确认:
# 用户要求退出
exit(0)
elif 'y' == 转码确认:
# 用户确认转码
break
else:
# 用户输入了 m,要求重新修改目标格式
pass
# endregion
目标视频文档路径列表: list[str] = [] # 用户记录目标视频文档的存放路径
# region 开始视频转码
编解码格式: str = ''
for 序号 in range(len(原视频文档列表)):
视频切片 = VideoFileClip(原视频文档列表[序号])
while not 目标视频文档存在性列表[序号]:
if not 目标格式后缀名.endswith('gif'):
# 如果不是要求转成 gif, 则需要指定编码格式
if not 编解码格式:
if 目标格式后缀名.endswith('mp3'):
# 如果要求转成 mp3 音频,则不需要指定编码格式
pass
elif 目标格式后缀名.endswith('wav'):
# 如果是要求转换成音频是 wav
编解码格式 = 交互接口类.发起文本交互(
输入提示=f'默认 codec 参数是 pcm_s32le, 输入 {绿字("y 以确认")}; '
f'或者您可以输入其它编码格式({红字("0")}: {红字("退出程序")}):',
画板=画板.副本.缩进()).lower()
if 'y' == 编解码格式:
# 用户确认使用 pcm_s32le 作为 codec 参数
编解码格式 = 'pcm_s32le'
else:
编解码格式 = 交互接口类.发起文本交互(
输入提示=f'默认 codec 参数是 libx264, 输入 {绿字("y 以确认")}; '
f'或者您可以输入其它编码格式({红字("0")}: {红字("退出程序")}):',
画板=画板.副本.缩进()).lower()
if 'y' == 编解码格式:
# 用户确认使用 libx264 作为 codec 参数
编解码格式 = 'libx264'
if '0' == 编解码格式:
# 用户要求退出程序
exit(0)
try:
if 目标格式后缀名.endswith('gif'):
# 转成gif图片
视频切片.write_gif(filename=目标视频文档列表[序号])
elif 目标格式后缀名.endswith('wav'):
# 转成 wav 音频
视频切片.audio.write_audiofile(filename=目标视频文档列表[序号], codec=编解码格式)
elif 目标格式后缀名.endswith('mp3'):
# 转成 mp3 音频
视频切片.audio.write_audiofile(filename=目标视频文档列表[序号])
elif 编解码格式:
# 根据指定的编码格式转成目标格式的视频文档
视频切片.write_videofile(filename=目标视频文档列表[序号], codec=编解码格式)
else:
# 不指定编码格式,尝试转成目标目标格式的视频文档
视频切片.write_videofile(filename=目标视频文档列表[序号])
except Exception as exp:
画板.提示错误('视频转码遇到异常如下:')
画板.消息(exp.__str__())
if 'Permission denied' in exp.__str__():
# 这可能是遇到了写入权限问题,此时需要引导用户指定新的目标视频存放地址
指定视频文档存放位置: str = 交互接口类.指定选择路径(输入提示=f'请指定视频转码文档的存放位置, 输入关键字以辅助定位路径:',
候选项上限=-1,
搜索接口=本地搜索,
画板=画板.副本.缩进())
if '0' == 指定视频文档存放位置:
# 用户要求退出程序
exit(0)
else:
当前目标视频存放位置: str = os.path.split(目标视频文档列表[序号])[0]
# 把 目标视频文档列表 中指向 当前目标视频存在位置 的目标视频文档命名重定向到新的指定的存放位置
for 子序号 in range(序号,len(目标视频文档列表)):
视频文档路径,视频文档名 = os.path.split(目标视频文档列表[子序号])
if 视频文档路径 == 当前目标视频存放位置:
目标视频文档列表[子序号] = os.path.join(指定视频文档存放位置,视频文档名)
目标视频文档存在性列表[子序号] = os.path.isfile(目标视频文档列表[子序号])
# 打印更新后的视频转码情况
画板.消息(黄字('转码过程更新如下:'))
画板.准备表格(对齐控制串='rl')
for 子序号 in range(len(原视频文档列表)):
原文档, 目标文档 = 差异标注(原视频文档列表[子序号], 目标视频文档列表[子序号])
画板.添加一行(
原文档, 洋红字('->'), 目标文档,
红字('已存在') if 目标视频文档存在性列表[子序号] else 绿字('待转码'))
画板.展示表格(列间距=[0, 0])
# 目标视频文档列表更新完成后, 重新尝试转码
else:
# 清除 codec 参数,重新发起交互
编解码格式 = ''
continue
else:
# 如果转码顺利完成,则关闭 视频切片,开始下一段视频的处理
视频切片.close()
目标视频文档路径: str = os.path.abspath(目标视频文档列表[序号])
if 目标视频文档路径 not in 目标视频文档路径列表:
# 目标视频文档路径列表.append(目标视频文档路径)
目标视频文档路径列表.append(目标视频文档路径)
break
# endregion
# region 确认是否协助用户打开目标视频文档路径
需要协助打开的路径列表: list[str] = [路径 for 路径 in 目标视频文档路径列表 if 路径 not in 已经打开的目标视频文档路径]
if 需要协助打开的路径列表:
用户决策: str = 交互接口类.发起文本交互(输入提示=f"是否需要打开目标文件夹?({绿字('y:确认打开')};"
f"{红字('n:不打开')};"
f"{红字('0:退出程序')})",
限定范围='YyNn0',
画板=画板.副本.缩进()).lower()
if 'y' == 用户决策:
for 路径 in 需要协助打开的路径列表:
通过totalCommander打开指定路径(路径=路径,
搜索接口=本地搜索,
画板=画板.副本.缩进())
已经打开的目标视频文档路径.append(路径)
elif '0' == 用户决策:
# 用户要求退出程序
exit(0)
# endregion
# endregion
使用演示
👉第一步,脚本首先引导用户搜索和定位视频文件,我们可以输入视频文件名信息以定位视频文档,然后参过序号选择目标文档;如下👇:
👉第二步,脚本询问我们需要将视频文档转换为什么样的目标格式,我们输入目标文档的后缀名,然后再一次确认脚本的问询;如下👇:
在这一步中,我们输入目标格式为 mp3,此时脚本列出了我们每个文件的转码预测,以及目标mp3是否已经存在,如果目标mp3文件已经存在,则脚本后续的转码过程中,就不会对该文件进行重复转码了。
👉第三步,脚本会询问用户是否确认转码,在获取用户确认后,脚本会对指定的视频文件逐一进行转码。如下👇:
👉第四步,脚本完成转码后,会询问用户是否需要打开目标文件路径,根据用户的决策结果来为用户打开目标文件路径,以便用户继续后续的操作。
以上👆脚本交互完成后,脚本即开始了视频转码过程,我们耐心等待转码完成即可,如下👇:
懒人包
以上 Python脚本,对应的bat调用脚本,和对应的cfg参数文档,统一进行了打包,见:Python 视频转mp3脚本。
获取用户确认后,脚本会对指定的视频文件逐一进行转码。如下👇:
[外链图片转存中…(img-Z1ZnaJ9L-1715740037940)]
👉第四步,脚本完成转码后,会询问用户是否需要打开目标文件路径,根据用户的决策结果来为用户打开目标文件路径,以便用户继续后续的操作。
以上👆脚本交互完成后,脚本即开始了视频转码过程,我们耐心等待转码完成即可,如下👇:
[外链图片转存中…(img-qAAfCgPH-1715740037940)]
懒人包
以上 Python脚本,对应的bat调用脚本,和对应的cfg参数文档,统一进行了打包,见:Python 视频转mp3脚本。