mirror of
https://github.com/woodchen-ink/video2gif.git
synced 2025-07-18 13:42:03 +08:00
Compare commits
23 Commits
v1.0.0-8d0
...
main
Author | SHA1 | Date | |
---|---|---|---|
5f5e5350a7 | |||
7808d7a555 | |||
78a6ec111f | |||
d407d3f72e | |||
163f71564f | |||
861b4af2d2 | |||
0f8751a2f2 | |||
302dbc805f | |||
6d8fb24807 | |||
|
06ba7c7cea | ||
7647d15c52 | |||
f0844c2284 | |||
91fe9bc3da | |||
0decff023e | |||
93a01d21cf | |||
e809a2e6f7 | |||
dac061da63 | |||
5d5decf8ff | |||
ba7fe58645 | |||
8788f0b0b9 | |||
f037cf9ddd | |||
79d10fcb89 | |||
1c42ad7043 |
11
.github/dependabot.yml
vendored
Normal file
11
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# To get started with Dependabot version updates, you'll need to specify which
|
||||||
|
# package ecosystems to update and where the package manifests are located.
|
||||||
|
# Please see the documentation for all configuration options:
|
||||||
|
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "pip" # See documentation for possible values
|
||||||
|
directory: "/" # Location of package manifests
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
12
.github/workflows/release.yml
vendored
12
.github/workflows/release.yml
vendored
@ -57,19 +57,13 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
# 确保安装tkinterdnd2
|
|
||||||
pip install tkinterdnd2>=0.4.2
|
|
||||||
pip install pyinstaller
|
pip install pyinstaller
|
||||||
|
|
||||||
# 创建启动器脚本
|
|
||||||
- name: Create launcher script
|
|
||||||
run: |
|
|
||||||
echo 'import os, sys; os.environ["PATH"] = os.path.join(os.path.dirname(sys.executable), "ffmpeg") + os.pathsep + os.environ["PATH"]' > launcher.py
|
|
||||||
type gui.py >> launcher.py
|
|
||||||
|
|
||||||
- name: Build with PyInstaller
|
- name: Build with PyInstaller
|
||||||
|
shell: cmd
|
||||||
run: |
|
run: |
|
||||||
pyinstaller --name video2gif --onefile --windowed --add-data "ffmpeg/ffmpeg.exe;ffmpeg" --add-data "ffmpeg/ffprobe.exe;ffmpeg" --add-data "README.md;." launcher.py
|
pyinstaller --name video2gif --onefile --windowed --hidden-import ffmpeg --add-data "ffmpeg/ffmpeg.exe;ffmpeg" --add-data "ffmpeg/ffprobe.exe;ffmpeg" --add-data "README.md;." --clean --noconfirm --log-level=INFO gui.py
|
||||||
|
|
||||||
|
|
||||||
- name: Upload Release Asset
|
- name: Upload Release Asset
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -1 +1,2 @@
|
|||||||
__pycache__/gui.cpython-311.pyc
|
__pycache__/gui.cpython-311.pyc
|
||||||
|
ffmpeg/ffmpeg.exe
|
||||||
|
@ -5,8 +5,8 @@
|
|||||||
## 功能
|
## 功能
|
||||||
|
|
||||||
- 支持拖放视频文件到应用程序窗口进行转换。
|
- 支持拖放视频文件到应用程序窗口进行转换。
|
||||||
- 自动检测FFmpeg是否安装,如未安装则提供安装向导。
|
- 内置FFmpeg工具进行视频转码, 不需要在系统安装FFmpeg。
|
||||||
- 提供多种系统平台的安装和使用说明。
|
- 多平台, 多线程。
|
||||||
|
|
||||||
## 安装
|
## 安装
|
||||||
|
|
||||||
@ -32,7 +32,7 @@
|
|||||||
|
|
||||||
### 环境搭建
|
### 环境搭建
|
||||||
|
|
||||||
1. 安装Python 3.7及以上版本。
|
1. 安装Python 3.10及以上版本。
|
||||||
2. 安装依赖库:
|
2. 安装依赖库:
|
||||||
```bash
|
```bash
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
|
677
gui.py
677
gui.py
@ -6,398 +6,60 @@ import platform
|
|||||||
import os
|
import os
|
||||||
import webbrowser
|
import webbrowser
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
import ffmpeg
|
import traceback
|
||||||
import traceback # 用于打印详细错误信息
|
|
||||||
|
|
||||||
# 根据平台选择拖放实现
|
# 设置 FFmpeg 路径
|
||||||
PLATFORM = platform.system().lower()
|
if getattr(sys, "frozen", False):
|
||||||
|
# 运行在 PyInstaller 打包后的环境
|
||||||
|
ffmpeg_path = os.path.join(sys._MEIPASS, "ffmpeg")
|
||||||
|
else:
|
||||||
|
# 运行在开发环境
|
||||||
|
ffmpeg_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ffmpeg")
|
||||||
|
|
||||||
if PLATFORM == "windows":
|
# 添加到系统 PATH
|
||||||
try:
|
if ffmpeg_path not in os.environ["PATH"]:
|
||||||
from tkinterdnd2 import DND_FILES, TkinterDnD
|
os.environ["PATH"] = ffmpeg_path + os.pathsep + os.environ["PATH"]
|
||||||
|
|
||||||
SUPPORT_DND = "tkdnd"
|
|
||||||
except ImportError:
|
|
||||||
SUPPORT_DND = None
|
|
||||||
elif PLATFORM == "darwin": # macOS
|
|
||||||
SUPPORT_DND = "macos"
|
|
||||||
else: # Linux or others
|
|
||||||
SUPPORT_DND = None
|
|
||||||
|
|
||||||
|
|
||||||
class FFmpegInstaller:
|
|
||||||
@staticmethod
|
|
||||||
def get_ffmpeg_path():
|
|
||||||
"""获取FFmpeg可执行文件路径"""
|
|
||||||
try:
|
|
||||||
# 获取程序运行路径
|
|
||||||
if getattr(sys, "frozen", False):
|
|
||||||
# PyInstaller打包后的路径
|
|
||||||
base_path = sys._MEIPASS
|
|
||||||
else:
|
|
||||||
# 开发环境路径
|
|
||||||
base_path = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
|
|
||||||
# FFmpeg路径
|
|
||||||
ffmpeg_dir = os.path.join(base_path, "ffmpeg")
|
|
||||||
if platform.system().lower() == "windows":
|
|
||||||
ffmpeg_path = os.path.join(ffmpeg_dir, "ffmpeg.exe")
|
|
||||||
else:
|
|
||||||
ffmpeg_path = os.path.join(ffmpeg_dir, "ffmpeg")
|
|
||||||
|
|
||||||
return ffmpeg_path
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error getting FFmpeg path: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def check_ffmpeg():
|
|
||||||
"""检查是否可以使用FFmpeg"""
|
|
||||||
try:
|
|
||||||
ffmpeg_path = FFmpegInstaller.get_ffmpeg_path()
|
|
||||||
if ffmpeg_path and os.path.exists(ffmpeg_path):
|
|
||||||
# 使用打包的FFmpeg
|
|
||||||
if platform.system().lower() == "windows":
|
|
||||||
startupinfo = subprocess.STARTUPINFO()
|
|
||||||
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
|
||||||
result = subprocess.run(
|
|
||||||
[ffmpeg_path, "-version"],
|
|
||||||
capture_output=True,
|
|
||||||
startupinfo=startupinfo,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
result = subprocess.run(
|
|
||||||
[ffmpeg_path, "-version"], capture_output=True
|
|
||||||
)
|
|
||||||
return result.returncode == 0
|
|
||||||
|
|
||||||
# 尝试系统PATH中的FFmpeg
|
|
||||||
if platform.system().lower() == "windows":
|
|
||||||
startupinfo = subprocess.STARTUPINFO()
|
|
||||||
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
|
||||||
result = subprocess.run(
|
|
||||||
["ffmpeg", "-version"], capture_output=True, startupinfo=startupinfo
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
result = subprocess.run(["ffmpeg", "-version"], capture_output=True)
|
|
||||||
return result.returncode == 0
|
|
||||||
|
|
||||||
except FileNotFoundError:
|
|
||||||
return False
|
|
||||||
except Exception as e:
|
|
||||||
print(f"FFmpeg检测出错: {str(e)}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_system_info():
|
|
||||||
"""获取系统信息"""
|
|
||||||
system = platform.system().lower()
|
|
||||||
is_64bits = sys.maxsize > 2**32
|
|
||||||
return system, is_64bits
|
|
||||||
|
|
||||||
def show_installation_dialog(self):
|
|
||||||
"""显示安装向导对话框"""
|
|
||||||
system, is_64bits = self.get_system_info()
|
|
||||||
|
|
||||||
dialog = tk.Toplevel()
|
|
||||||
dialog.title("FFmpeg 安装向导")
|
|
||||||
dialog.geometry("500x400")
|
|
||||||
dialog.transient() # 设置为模态窗口
|
|
||||||
|
|
||||||
# 添加说明文本
|
|
||||||
ttk.Label(
|
|
||||||
dialog,
|
|
||||||
text="未检测到 FFmpeg,需要先安装 FFmpeg 才能继续使用。",
|
|
||||||
wraplength=450,
|
|
||||||
).pack(padx=20, pady=10)
|
|
||||||
|
|
||||||
if system == "windows":
|
|
||||||
self._setup_windows_installer(dialog)
|
|
||||||
elif system == "darwin": # macOS
|
|
||||||
self._setup_macos_installer(dialog)
|
|
||||||
elif system == "linux":
|
|
||||||
self._setup_linux_installer(dialog)
|
|
||||||
else:
|
|
||||||
self._setup_manual_installer(dialog)
|
|
||||||
|
|
||||||
dialog.focus_set()
|
|
||||||
return dialog
|
|
||||||
|
|
||||||
def _setup_windows_installer(self, dialog):
|
|
||||||
"""Windows安装方式"""
|
|
||||||
ttk.Label(dialog, text="Windows 安装方式:", font=("", 10, "bold")).pack(
|
|
||||||
padx=20, pady=5
|
|
||||||
)
|
|
||||||
|
|
||||||
# 方式1: Chocolatey
|
|
||||||
choco_frame = ttk.LabelFrame(dialog, text="方式1:使用 Chocolatey(推荐)")
|
|
||||||
choco_frame.pack(padx=20, pady=5, fill="x")
|
|
||||||
|
|
||||||
ttk.Label(
|
|
||||||
choco_frame,
|
|
||||||
text="1. 首先安装 Chocolatey,在管理员权限的 PowerShell 中运行:",
|
|
||||||
wraplength=450,
|
|
||||||
).pack(padx=5, pady=5)
|
|
||||||
|
|
||||||
choco_cmd = """Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))"""
|
|
||||||
cmd_text = ttk.Entry(choco_frame, width=50)
|
|
||||||
cmd_text.insert(0, choco_cmd)
|
|
||||||
cmd_text.pack(padx=5, pady=5)
|
|
||||||
|
|
||||||
ttk.Button(
|
|
||||||
choco_frame,
|
|
||||||
text="复制命令",
|
|
||||||
command=lambda: self._copy_to_clipboard(choco_cmd),
|
|
||||||
).pack(pady=5)
|
|
||||||
|
|
||||||
ttk.Label(choco_frame, text="2. 然后安装 FFmpeg,运行:").pack(padx=5, pady=5)
|
|
||||||
|
|
||||||
ffmpeg_cmd = "choco install ffmpeg"
|
|
||||||
cmd_text2 = ttk.Entry(choco_frame, width=50)
|
|
||||||
cmd_text2.insert(0, ffmpeg_cmd)
|
|
||||||
cmd_text2.pack(padx=5, pady=5)
|
|
||||||
|
|
||||||
ttk.Button(
|
|
||||||
choco_frame,
|
|
||||||
text="复制命令",
|
|
||||||
command=lambda: self._copy_to_clipboard(ffmpeg_cmd),
|
|
||||||
).pack(pady=5)
|
|
||||||
|
|
||||||
# 方式2: 直接下载
|
|
||||||
direct_frame = ttk.LabelFrame(dialog, text="方式2:直接下载")
|
|
||||||
direct_frame.pack(padx=20, pady=5, fill="x")
|
|
||||||
|
|
||||||
ttk.Label(direct_frame, text="从官方网站下载并手动配置环境变量:").pack(
|
|
||||||
padx=5, pady=5
|
|
||||||
)
|
|
||||||
|
|
||||||
ttk.Button(
|
|
||||||
direct_frame,
|
|
||||||
text="打开下载页面",
|
|
||||||
command=lambda: webbrowser.open("https://ffmpeg.org/download.html"),
|
|
||||||
).pack(pady=5)
|
|
||||||
|
|
||||||
def _setup_macos_installer(self, dialog):
|
|
||||||
"""macOS安装方式"""
|
|
||||||
ttk.Label(dialog, text="macOS 安装方式:", font=("", 10, "bold")).pack(
|
|
||||||
padx=20, pady=5
|
|
||||||
)
|
|
||||||
|
|
||||||
# 方式1: Homebrew
|
|
||||||
brew_frame = ttk.LabelFrame(dialog, text="方式1:使用 Homebrew(推荐)")
|
|
||||||
brew_frame.pack(padx=20, pady=5, fill="x")
|
|
||||||
|
|
||||||
ttk.Label(
|
|
||||||
brew_frame, text="1. 首先安装 Homebrew,在终端运行:", wraplength=450
|
|
||||||
).pack(padx=5, pady=5)
|
|
||||||
|
|
||||||
brew_cmd = '/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"'
|
|
||||||
cmd_text = ttk.Entry(brew_frame, width=50)
|
|
||||||
cmd_text.insert(0, brew_cmd)
|
|
||||||
cmd_text.pack(padx=5, pady=5)
|
|
||||||
|
|
||||||
ttk.Button(
|
|
||||||
brew_frame,
|
|
||||||
text="复制命令",
|
|
||||||
command=lambda: self._copy_to_clipboard(brew_cmd),
|
|
||||||
).pack(pady=5)
|
|
||||||
|
|
||||||
ttk.Label(brew_frame, text="2. 然后安装 FFmpeg,运行:").pack(padx=5, pady=5)
|
|
||||||
|
|
||||||
ffmpeg_cmd = "brew install ffmpeg"
|
|
||||||
cmd_text2 = ttk.Entry(brew_frame, width=50)
|
|
||||||
cmd_text2.insert(0, ffmpeg_cmd)
|
|
||||||
cmd_text2.pack(padx=5, pady=5)
|
|
||||||
|
|
||||||
ttk.Button(
|
|
||||||
brew_frame,
|
|
||||||
text="复制命令",
|
|
||||||
command=lambda: self._copy_to_clipboard(ffmpeg_cmd),
|
|
||||||
).pack(pady=5)
|
|
||||||
|
|
||||||
def _setup_linux_installer(self, dialog):
|
|
||||||
"""Linux安装方式"""
|
|
||||||
ttk.Label(dialog, text="Linux 安装方式:", font=("", 10, "bold")).pack(
|
|
||||||
padx=20, pady=5
|
|
||||||
)
|
|
||||||
|
|
||||||
# Ubuntu/Debian
|
|
||||||
ubuntu_frame = ttk.LabelFrame(dialog, text="Ubuntu/Debian")
|
|
||||||
ubuntu_frame.pack(padx=20, pady=5, fill="x")
|
|
||||||
|
|
||||||
ubuntu_cmd = "sudo apt update && sudo apt install ffmpeg"
|
|
||||||
cmd_text = ttk.Entry(ubuntu_frame, width=50)
|
|
||||||
cmd_text.insert(0, ubuntu_cmd)
|
|
||||||
cmd_text.pack(padx=5, pady=5)
|
|
||||||
|
|
||||||
ttk.Button(
|
|
||||||
ubuntu_frame,
|
|
||||||
text="复制命令",
|
|
||||||
command=lambda: self._copy_to_clipboard(ubuntu_cmd),
|
|
||||||
).pack(pady=5)
|
|
||||||
|
|
||||||
# CentOS/RHEL
|
|
||||||
centos_frame = ttk.LabelFrame(dialog, text="CentOS/RHEL")
|
|
||||||
centos_frame.pack(padx=20, pady=5, fill="x")
|
|
||||||
|
|
||||||
centos_cmd = "sudo yum install epel-release && sudo yum install ffmpeg"
|
|
||||||
cmd_text2 = ttk.Entry(centos_frame, width=50)
|
|
||||||
cmd_text2.insert(0, centos_cmd)
|
|
||||||
cmd_text2.pack(padx=5, pady=5)
|
|
||||||
|
|
||||||
ttk.Button(
|
|
||||||
centos_frame,
|
|
||||||
text="复制命令",
|
|
||||||
command=lambda: self._copy_to_clipboard(centos_cmd),
|
|
||||||
).pack(pady=5)
|
|
||||||
|
|
||||||
def _setup_manual_installer(self, dialog):
|
|
||||||
"""手动安装说明"""
|
|
||||||
ttk.Label(
|
|
||||||
dialog, text="请访问 FFmpeg 官方网站下载并安装:", wraplength=450
|
|
||||||
).pack(padx=20, pady=10)
|
|
||||||
|
|
||||||
ttk.Button(
|
|
||||||
dialog,
|
|
||||||
text="打开 FFmpeg 官方网站",
|
|
||||||
command=lambda: webbrowser.open("https://ffmpeg.org/download.html"),
|
|
||||||
).pack(pady=10)
|
|
||||||
|
|
||||||
def _copy_to_clipboard(self, text):
|
|
||||||
"""复制文本到剪贴板"""
|
|
||||||
dialog = tk.Toplevel()
|
|
||||||
dialog.withdraw()
|
|
||||||
dialog.clipboard_clear()
|
|
||||||
dialog.clipboard_append(text)
|
|
||||||
dialog.update()
|
|
||||||
dialog.destroy()
|
|
||||||
messagebox.showinfo("提示", "命令已复制到剪贴板!")
|
|
||||||
|
|
||||||
|
|
||||||
class VideoToGifConverter:
|
class VideoToGifConverter:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.check_ffmpeg_installation()
|
|
||||||
|
|
||||||
# 创建主窗口
|
# 创建主窗口
|
||||||
if SUPPORT_DND == "tkdnd":
|
self.root = tk.Tk()
|
||||||
self.root = TkinterDnD.Tk()
|
|
||||||
else:
|
|
||||||
self.root = tk.Tk()
|
|
||||||
|
|
||||||
self.root.title("视频转GIF工具")
|
self.root.title("视频转GIF工具")
|
||||||
|
|
||||||
# 设置拖放支持
|
# 设置窗口大小和位置
|
||||||
self.setup_dnd()
|
window_width = 800
|
||||||
|
window_height = 600
|
||||||
|
screen_width = self.root.winfo_screenwidth()
|
||||||
|
screen_height = self.root.winfo_screenheight()
|
||||||
|
x = (screen_width - window_width) // 2
|
||||||
|
y = (screen_height - window_height) // 2
|
||||||
|
self.root.geometry(f"{window_width}x{window_height}+{x}+{y}")
|
||||||
|
# 设置窗口样式
|
||||||
|
self.root.configure(bg="#f0f0f0")
|
||||||
|
style = ttk.Style()
|
||||||
|
style.configure("TButton", padding=6)
|
||||||
|
style.configure("TLabelframe", background="#f0f0f0")
|
||||||
|
|
||||||
# 设置UI
|
# 设置UI
|
||||||
self.setup_ui()
|
self.setup_ui()
|
||||||
|
|
||||||
def setup_dnd(self):
|
|
||||||
"""设置拖放支持"""
|
|
||||||
if SUPPORT_DND == "tkdnd":
|
|
||||||
# Windows with tkinterdnd2
|
|
||||||
self.root.drop_target_register(DND_FILES)
|
|
||||||
self.root.dnd_bind("<<Drop>>", self.handle_drop)
|
|
||||||
elif SUPPORT_DND == "macos":
|
|
||||||
# macOS native drag and drop
|
|
||||||
self.root.bind("<<MacDropFiles>>", self.handle_macos_drop)
|
|
||||||
# 启用macOS拖放
|
|
||||||
self.root.tk.call("tk_getOpenFile", "-setup")
|
|
||||||
else:
|
|
||||||
print("Drag and drop not supported on this platform")
|
|
||||||
|
|
||||||
def handle_drop(self, event):
|
|
||||||
"""处理Windows下的文件拖放"""
|
|
||||||
files = self.root.tk.splitlist(event.data)
|
|
||||||
self.process_dropped_files(files)
|
|
||||||
|
|
||||||
def handle_macos_drop(self, event):
|
|
||||||
"""处理macOS下的文件拖放"""
|
|
||||||
# macOS下获取拖放的文件路径
|
|
||||||
files = self.root.tk.splitlist(self.root.tk.call("::tk::mac::GetDroppedFiles"))
|
|
||||||
self.process_dropped_files(files)
|
|
||||||
|
|
||||||
def process_dropped_files(self, files):
|
|
||||||
"""处理拖放的文件"""
|
|
||||||
# 过滤出视频文件
|
|
||||||
valid_extensions = (".mp4", ".avi", ".mov", ".mkv")
|
|
||||||
valid_files = [f for f in files if f.lower().endswith(valid_extensions)]
|
|
||||||
|
|
||||||
if valid_files:
|
|
||||||
self.files_list.delete(0, tk.END)
|
|
||||||
for file in valid_files:
|
|
||||||
self.files_list.insert(tk.END, file)
|
|
||||||
else:
|
|
||||||
messagebox.showwarning(
|
|
||||||
"警告", "请拖入视频文件(支持mp4, avi, mov, mkv格式)"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 添加拖放处理方法
|
|
||||||
def handle_drop(self, event):
|
|
||||||
"""处理文件拖放"""
|
|
||||||
files = self.root.tk.splitlist(event.data)
|
|
||||||
# 过滤出视频文件
|
|
||||||
valid_extensions = (".mp4", ".avi", ".mov", ".mkv")
|
|
||||||
valid_files = [f for f in files if f.lower().endswith(valid_extensions)]
|
|
||||||
|
|
||||||
if valid_files:
|
|
||||||
self.files_list.delete(0, tk.END)
|
|
||||||
for file in valid_files:
|
|
||||||
self.files_list.insert(tk.END, file)
|
|
||||||
else:
|
|
||||||
messagebox.showwarning(
|
|
||||||
"警告", "请拖入视频文件(支持mp4, avi, mov, mkv格式)"
|
|
||||||
)
|
|
||||||
|
|
||||||
def check_ffmpeg_installation(self):
|
|
||||||
"""检查FFmpeg是否已安装"""
|
|
||||||
installer = FFmpegInstaller()
|
|
||||||
if not installer.check_ffmpeg():
|
|
||||||
# 创建一个临时的root窗口来显示消息框
|
|
||||||
temp_root = tk.Tk()
|
|
||||||
temp_root.withdraw() # 隐藏临时窗口
|
|
||||||
|
|
||||||
response = messagebox.askquestion(
|
|
||||||
"FFmpeg未安装", "需要安装FFmpeg才能使用本工具。是否查看安装指南?"
|
|
||||||
)
|
|
||||||
|
|
||||||
if response == "yes":
|
|
||||||
dialog = installer.show_installation_dialog()
|
|
||||||
temp_root.wait_window(dialog) # 等待安装向导窗口关闭
|
|
||||||
|
|
||||||
# 再次检查安装
|
|
||||||
if not installer.check_ffmpeg():
|
|
||||||
if (
|
|
||||||
messagebox.askquestion(
|
|
||||||
"提示", "请安装完FFmpeg后再运行程序。是否退出程序?"
|
|
||||||
)
|
|
||||||
== "yes"
|
|
||||||
):
|
|
||||||
temp_root.destroy()
|
|
||||||
sys.exit()
|
|
||||||
else:
|
|
||||||
temp_root.destroy()
|
|
||||||
sys.exit()
|
|
||||||
|
|
||||||
temp_root.destroy()
|
|
||||||
|
|
||||||
def setup_ui(self):
|
def setup_ui(self):
|
||||||
# 文件选择框
|
# 文件选择框
|
||||||
self.file_frame = ttk.LabelFrame(self.root, text="选择文件")
|
self.file_frame = ttk.LabelFrame(self.root, text="选择文件")
|
||||||
self.file_frame.pack(padx=10, pady=5, fill="x")
|
self.file_frame.pack(padx=10, pady=5, fill="x")
|
||||||
|
|
||||||
# 添加拖放提示
|
|
||||||
if SUPPORT_DND:
|
|
||||||
ttk.Label(
|
|
||||||
self.file_frame, text="可以直接拖放视频文件到此处", foreground="blue"
|
|
||||||
).pack(pady=5)
|
|
||||||
|
|
||||||
self.files_list = tk.Listbox(self.file_frame, height=5)
|
self.files_list = tk.Listbox(self.file_frame, height=5)
|
||||||
self.files_list.pack(padx=5, pady=5, fill="x")
|
self.files_list.pack(padx=5, pady=5, fill="x")
|
||||||
|
|
||||||
# 如果是macOS,为Listbox添加拖放支持
|
# 为文件列表添加右键菜单
|
||||||
if SUPPORT_DND == "macos":
|
self.files_list_menu = tk.Menu(self.root, tearoff=0)
|
||||||
self.files_list.bind("<<MacDropFiles>>", self.handle_macos_drop)
|
self.files_list_menu.add_command(label="删除选中", command=self.delete_selected)
|
||||||
|
self.files_list_menu.add_command(
|
||||||
|
label="清空列表", command=lambda: self.files_list.delete(0, tk.END)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.files_list.bind("<Button-3>", self.show_context_menu)
|
||||||
|
|
||||||
btn_frame = ttk.Frame(self.file_frame)
|
btn_frame = ttk.Frame(self.file_frame)
|
||||||
btn_frame.pack(fill="x", padx=5, pady=5)
|
btn_frame.pack(fill="x", padx=5, pady=5)
|
||||||
@ -478,22 +140,6 @@ class VideoToGifConverter:
|
|||||||
self.duration_entry.pack(side="left", padx=5)
|
self.duration_entry.pack(side="left", padx=5)
|
||||||
ttk.Label(duration_frame, text="(留空表示全部)").pack(side="left", padx=5)
|
ttk.Label(duration_frame, text="(留空表示全部)").pack(side="left", padx=5)
|
||||||
|
|
||||||
# 质量设置
|
|
||||||
quality_frame = ttk.Frame(self.settings_frame)
|
|
||||||
quality_frame.pack(fill="x", padx=5, pady=5)
|
|
||||||
|
|
||||||
ttk.Label(quality_frame, text="质量设置:").pack(side="left", padx=5)
|
|
||||||
self.quality_var = tk.StringVar(value="medium")
|
|
||||||
ttk.Radiobutton(
|
|
||||||
quality_frame, text="高质量", variable=self.quality_var, value="high"
|
|
||||||
).pack(side="left", padx=5)
|
|
||||||
ttk.Radiobutton(
|
|
||||||
quality_frame, text="中等", variable=self.quality_var, value="medium"
|
|
||||||
).pack(side="left", padx=5)
|
|
||||||
ttk.Radiobutton(
|
|
||||||
quality_frame, text="低质量", variable=self.quality_var, value="low"
|
|
||||||
).pack(side="left", padx=5)
|
|
||||||
|
|
||||||
# 输出设置
|
# 输出设置
|
||||||
output_frame = ttk.Frame(self.settings_frame)
|
output_frame = ttk.Frame(self.settings_frame)
|
||||||
output_frame.pack(fill="x", padx=5, pady=5)
|
output_frame.pack(fill="x", padx=5, pady=5)
|
||||||
@ -531,14 +177,19 @@ class VideoToGifConverter:
|
|||||||
# 状态标签
|
# 状态标签
|
||||||
self.status_label = ttk.Label(self.root, text="就绪")
|
self.status_label = ttk.Label(self.root, text="就绪")
|
||||||
self.status_label.pack(pady=5)
|
self.status_label.pack(pady=5)
|
||||||
# 如果不支持拖放,添加提示
|
|
||||||
if not SUPPORT_DND:
|
def show_context_menu(self, event):
|
||||||
ttk.Label(
|
"""显示右键菜单"""
|
||||||
self.file_frame,
|
try:
|
||||||
text="注意:当前版本不支持拖放功能,请使用'选择视频'按钮",
|
self.files_list_menu.tk_popup(event.x_root, event.y_root)
|
||||||
wraplength=300,
|
finally:
|
||||||
foreground="red",
|
self.files_list_menu.grab_release()
|
||||||
).pack(pady=5)
|
|
||||||
|
def delete_selected(self):
|
||||||
|
"""删除选中的文件"""
|
||||||
|
selection = self.files_list.curselection()
|
||||||
|
for index in reversed(selection):
|
||||||
|
self.files_list.delete(index)
|
||||||
|
|
||||||
def browse_output(self):
|
def browse_output(self):
|
||||||
directory = filedialog.askdirectory()
|
directory = filedialog.askdirectory()
|
||||||
@ -553,23 +204,47 @@ class VideoToGifConverter:
|
|||||||
for file in files:
|
for file in files:
|
||||||
self.files_list.insert(tk.END, file)
|
self.files_list.insert(tk.END, file)
|
||||||
|
|
||||||
def get_quality_settings(self):
|
def validate_inputs(self):
|
||||||
"""根据质量设置返回 FFmpeg 参数"""
|
"""验证输入参数"""
|
||||||
quality = self.quality_var.get()
|
try:
|
||||||
if quality == "high":
|
# 验证FPS
|
||||||
return ["-quality", "100"]
|
fps = int(self.fps_var.get())
|
||||||
elif quality == "medium":
|
if fps <= 0:
|
||||||
return ["-quality", "75"]
|
raise ValueError("FPS必须大于0")
|
||||||
else:
|
|
||||||
return ["-quality", "50"]
|
# 验证时间设置
|
||||||
|
start_time = float(self.start_time_var.get() or 0)
|
||||||
|
if start_time < 0:
|
||||||
|
raise ValueError("开始时间不能为负数")
|
||||||
|
|
||||||
|
if self.duration_var.get():
|
||||||
|
duration = float(self.duration_var.get())
|
||||||
|
if duration <= 0:
|
||||||
|
raise ValueError("持续时间必须大于0")
|
||||||
|
|
||||||
|
# 验证自定义尺寸
|
||||||
|
if self.size_var.get() == "custom":
|
||||||
|
width = int(self.width_var.get())
|
||||||
|
if width <= 0:
|
||||||
|
raise ValueError("宽度必须大于0")
|
||||||
|
|
||||||
|
height = self.height_var.get()
|
||||||
|
if height != "auto":
|
||||||
|
height = int(height)
|
||||||
|
if height <= 0:
|
||||||
|
raise ValueError("高度必须大于0")
|
||||||
|
|
||||||
|
return True
|
||||||
|
except ValueError as e:
|
||||||
|
messagebox.showerror("输入错误", str(e))
|
||||||
|
return False
|
||||||
|
|
||||||
# 修改 convert_video_to_gif 方法
|
|
||||||
def convert_video_to_gif(self, video_path):
|
def convert_video_to_gif(self, video_path):
|
||||||
try:
|
try:
|
||||||
ffmpeg_path = FFmpegInstaller.get_ffmpeg_path()
|
# 验证输入
|
||||||
if ffmpeg_path:
|
if not self.validate_inputs():
|
||||||
# 设置FFmpeg路径
|
return False
|
||||||
ffmpeg.input.DEFAULT_FFMPEG_PATH = ffmpeg_path
|
|
||||||
# 确定输出路径
|
# 确定输出路径
|
||||||
if self.output_var.get() == "same":
|
if self.output_var.get() == "same":
|
||||||
output_dir = os.path.dirname(video_path)
|
output_dir = os.path.dirname(video_path)
|
||||||
@ -594,54 +269,116 @@ class VideoToGifConverter:
|
|||||||
cpu_count = os.cpu_count() or 1
|
cpu_count = os.cpu_count() or 1
|
||||||
threads = max(1, min(cpu_count - 1, 8)) # 留一个核心给系统用
|
threads = max(1, min(cpu_count - 1, 8)) # 留一个核心给系统用
|
||||||
|
|
||||||
# 第一步:生成调色板(添加线程参数)
|
# 获取ffmpeg路径
|
||||||
stream = ffmpeg.input(video_path)
|
ffmpeg_exe = os.path.join(
|
||||||
|
ffmpeg_path,
|
||||||
|
"ffmpeg.exe" if platform.system().lower() == "windows" else "ffmpeg",
|
||||||
|
)
|
||||||
|
|
||||||
if start_time > 0:
|
# 构建基本命令
|
||||||
stream = ffmpeg.filter(stream, "setpts", f"PTS-{start_time}/TB")
|
filter_complex = f"fps={fps}" # 开始构建滤镜链
|
||||||
|
|
||||||
if duration:
|
|
||||||
stream = ffmpeg.filter(stream, "t", duration=float(duration))
|
|
||||||
|
|
||||||
stream = ffmpeg.filter(stream, "fps", fps=fps)
|
|
||||||
|
|
||||||
|
# 添加尺寸控制到滤镜链
|
||||||
if self.size_var.get() == "custom":
|
if self.size_var.get() == "custom":
|
||||||
width = int(self.width_var.get())
|
width = int(self.width_var.get())
|
||||||
height = self.height_var.get()
|
height = self.height_var.get()
|
||||||
height = -1 if height == "auto" else int(height)
|
height = -1 if height == "auto" else int(height)
|
||||||
stream = ffmpeg.filter(stream, "scale", width=width, height=height)
|
filter_complex += f",scale={width}:{height}"
|
||||||
|
|
||||||
stream = ffmpeg.filter(stream, "palettegen")
|
# 更新状态显示
|
||||||
# 添加线程参数
|
self.status_label.config(
|
||||||
ffmpeg.run(
|
text=f"正在生成调色板... {os.path.basename(video_path)}"
|
||||||
ffmpeg.output(stream, palette_path, threads=threads),
|
|
||||||
overwrite_output=True,
|
|
||||||
)
|
)
|
||||||
|
self.root.update()
|
||||||
|
|
||||||
# 第二步:使用调色板生成GIF(添加线程参数)
|
# 构建调色板生成命令
|
||||||
stream = ffmpeg.input(video_path)
|
palette_cmd = [ffmpeg_exe, "-y", "-threads", str(threads)] # 覆盖输出文件
|
||||||
palette = ffmpeg.input(palette_path)
|
|
||||||
|
|
||||||
|
# 添加时间控制
|
||||||
if start_time > 0:
|
if start_time > 0:
|
||||||
stream = ffmpeg.filter(stream, "setpts", f"PTS-{start_time}/TB")
|
palette_cmd.extend(["-ss", str(start_time)])
|
||||||
|
|
||||||
|
palette_cmd.extend(["-i", video_path])
|
||||||
|
|
||||||
if duration:
|
if duration:
|
||||||
stream = ffmpeg.filter(stream, "t", duration=float(duration))
|
palette_cmd.extend(["-t", str(float(duration))])
|
||||||
|
|
||||||
stream = ffmpeg.filter(stream, "fps", fps=fps)
|
# 添加滤镜和输出
|
||||||
|
palette_cmd.extend(["-vf", f"{filter_complex},palettegen", palette_path])
|
||||||
|
|
||||||
if self.size_var.get() == "custom":
|
# 打印命令用于调试
|
||||||
width = int(self.width_var.get())
|
print("调色板生成命令:", " ".join(palette_cmd))
|
||||||
height = self.height_var.get()
|
|
||||||
height = -1 if height == "auto" else int(height)
|
|
||||||
stream = ffmpeg.filter(stream, "scale", width=width, height=height)
|
|
||||||
|
|
||||||
stream = ffmpeg.filter([stream, palette], "paletteuse")
|
# 创建 startupinfo 对象(用于隐藏 CMD 窗口)
|
||||||
# 添加线程参数
|
startupinfo = None
|
||||||
ffmpeg.run(
|
if platform.system().lower() == "windows":
|
||||||
ffmpeg.output(stream, output_path, threads=threads),
|
startupinfo = subprocess.STARTUPINFO()
|
||||||
overwrite_output=True,
|
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||||||
|
startupinfo.wShowWindow = subprocess.SW_HIDE
|
||||||
|
# 运行调色板生成命令
|
||||||
|
process = subprocess.Popen(
|
||||||
|
palette_cmd,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
startupinfo=startupinfo,
|
||||||
|
creationflags=(
|
||||||
|
subprocess.CREATE_NO_WINDOW
|
||||||
|
if platform.system().lower() == "windows"
|
||||||
|
else 0
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
_, stderr = process.communicate()
|
||||||
|
|
||||||
|
if process.returncode != 0:
|
||||||
|
raise RuntimeError(f"调色板生成失败: {stderr.decode()}")
|
||||||
|
|
||||||
|
# 更新状态显示
|
||||||
|
self.status_label.config(
|
||||||
|
text=f"正在生成GIF... {os.path.basename(video_path)}"
|
||||||
|
)
|
||||||
|
self.root.update()
|
||||||
|
|
||||||
|
# 构建GIF生成命令
|
||||||
|
gif_cmd = [ffmpeg_exe, "-y", "-threads", str(threads)]
|
||||||
|
|
||||||
|
# 添加时间控制
|
||||||
|
if start_time > 0:
|
||||||
|
gif_cmd.extend(["-ss", str(start_time)])
|
||||||
|
|
||||||
|
gif_cmd.extend(["-i", video_path])
|
||||||
|
|
||||||
|
if duration:
|
||||||
|
gif_cmd.extend(["-t", str(float(duration))])
|
||||||
|
|
||||||
|
gif_cmd.extend(
|
||||||
|
[
|
||||||
|
"-i",
|
||||||
|
palette_path,
|
||||||
|
"-lavfi",
|
||||||
|
f"{filter_complex} [x]; [x][1:v] paletteuse",
|
||||||
|
output_path,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# 打印命令用于调试
|
||||||
|
print("GIF生成命令:", " ".join(gif_cmd))
|
||||||
|
|
||||||
|
# 运行GIF生成命令
|
||||||
|
process = subprocess.Popen(
|
||||||
|
gif_cmd,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
startupinfo=startupinfo,
|
||||||
|
creationflags=(
|
||||||
|
subprocess.CREATE_NO_WINDOW
|
||||||
|
if platform.system().lower() == "windows"
|
||||||
|
else 0
|
||||||
|
),
|
||||||
|
)
|
||||||
|
_, stderr = process.communicate()
|
||||||
|
|
||||||
|
if process.returncode != 0:
|
||||||
|
raise RuntimeError(f"GIF生成失败: {stderr.decode()}")
|
||||||
|
|
||||||
# 删除临时调色板文件
|
# 删除临时调色板文件
|
||||||
if os.path.exists(palette_path):
|
if os.path.exists(palette_path):
|
||||||
@ -652,30 +389,51 @@ class VideoToGifConverter:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = str(e)
|
error_msg = str(e)
|
||||||
print(f"Conversion error: {error_msg}")
|
print(f"Conversion error: {error_msg}")
|
||||||
|
print(f"Error type: {type(e)}")
|
||||||
|
traceback.print_exc()
|
||||||
messagebox.showerror("错误", f"转换失败:\n{error_msg}")
|
messagebox.showerror("错误", f"转换失败:\n{error_msg}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def start_conversion(self):
|
def start_conversion(self):
|
||||||
def convert():
|
def convert():
|
||||||
files = self.files_list.get(0, tk.END)
|
try:
|
||||||
if not files:
|
files = self.files_list.get(0, tk.END)
|
||||||
messagebox.showwarning("警告", "请先选择要转换的视频文件")
|
if not files:
|
||||||
return
|
messagebox.showwarning("警告", "请先选择要转换的视频文件")
|
||||||
|
return
|
||||||
|
|
||||||
total = len(files)
|
total = len(files)
|
||||||
|
success_count = 0
|
||||||
|
|
||||||
for i, file in enumerate(files):
|
for i, file in enumerate(files):
|
||||||
current = i + 1
|
current = i + 1
|
||||||
self.status_label.config(
|
self.status_label.config(
|
||||||
text=f"正在转换: {os.path.basename(file)} ({current}/{total})"
|
text=f"正在转换: {os.path.basename(file)} ({current}/{total})"
|
||||||
)
|
)
|
||||||
success = self.convert_video_to_gif(file)
|
if self.convert_video_to_gif(file):
|
||||||
progress = current / total * 100
|
success_count += 1
|
||||||
self.progress["value"] = progress
|
progress = current / total * 100
|
||||||
|
self.progress["value"] = progress
|
||||||
|
|
||||||
self.status_label.config(text=f"转换完成 ({total}/{total})")
|
self.status_label.config(text=f"转换完成 ({success_count}/{total})")
|
||||||
self.convert_btn["state"] = "normal"
|
self.convert_btn["state"] = "normal"
|
||||||
messagebox.showinfo("完成", f"所有文件转换完成!\n成功转换 {total} 个文件")
|
|
||||||
|
if success_count == total:
|
||||||
|
messagebox.showinfo(
|
||||||
|
"完成", f"所有文件转换完成!\n成功转换 {total} 个文件"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
messagebox.showwarning(
|
||||||
|
"完成",
|
||||||
|
f"转换完成,但有部分失败。\n成功:{success_count}/{total}",
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Conversion error: {str(e)}")
|
||||||
|
traceback.print_exc()
|
||||||
|
messagebox.showerror("错误", f"转换过程出错:\n{str(e)}")
|
||||||
|
finally:
|
||||||
|
self.convert_btn["state"] = "normal"
|
||||||
|
|
||||||
self.convert_btn["state"] = "disabled"
|
self.convert_btn["state"] = "disabled"
|
||||||
self.progress["value"] = 0
|
self.progress["value"] = 0
|
||||||
@ -688,13 +446,6 @@ class VideoToGifConverter:
|
|||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
try:
|
try:
|
||||||
print("程序启动...")
|
print("程序启动...")
|
||||||
# 先单独测试 FFmpeg
|
|
||||||
installer = FFmpegInstaller()
|
|
||||||
ffmpeg_installed = installer.check_ffmpeg()
|
|
||||||
print(f"FFmpeg 检测结果: {ffmpeg_installed}")
|
|
||||||
|
|
||||||
if not ffmpeg_installed:
|
|
||||||
print("FFmpeg 未安装,将显示安装向导...")
|
|
||||||
|
|
||||||
app = VideoToGifConverter()
|
app = VideoToGifConverter()
|
||||||
app.run()
|
app.run()
|
||||||
|
BIN
icons/favicon.ico
Normal file
BIN
icons/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
BIN
icons/logo800.icns
Normal file
BIN
icons/logo800.icns
Normal file
Binary file not shown.
BIN
icons/logo800.jpg
Normal file
BIN
icons/logo800.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 57 KiB |
@ -1,7 +1,2 @@
|
|||||||
# 核心依赖
|
# 核心依赖
|
||||||
ffmpeg-python>=0.2.0
|
|
||||||
Pillow>=9.0.0
|
Pillow>=9.0.0
|
||||||
|
|
||||||
# Windows平台依赖
|
|
||||||
tkinterdnd2>=0.4.2; platform_system=="Windows"
|
|
||||||
# Mac平台可以使用其他方案,暂时不需要特殊依赖
|
|
||||||
|
45
video2gif.spec
Normal file
45
video2gif.spec
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# video2gif.spec
|
||||||
|
block_cipher = None
|
||||||
|
|
||||||
|
a = Analysis(
|
||||||
|
['gui.py'],
|
||||||
|
pathex=[],
|
||||||
|
binaries=[],
|
||||||
|
datas=[
|
||||||
|
('ffmpeg/ffmpeg.exe', 'ffmpeg'),
|
||||||
|
('ffmpeg/ffprobe.exe', 'ffmpeg'),
|
||||||
|
('README.md', '.')
|
||||||
|
],
|
||||||
|
hiddenimports=['ffmpeg', 'ffmpeg-python'],
|
||||||
|
hookspath=[],
|
||||||
|
hooksconfig={},
|
||||||
|
runtime_hooks=[],
|
||||||
|
excludes=[],
|
||||||
|
win_no_prefer_redirects=False,
|
||||||
|
win_private_assemblies=False,
|
||||||
|
cipher=block_cipher,
|
||||||
|
noarchive=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
||||||
|
|
||||||
|
exe = EXE(
|
||||||
|
pyz,
|
||||||
|
a.scripts,
|
||||||
|
a.binaries,
|
||||||
|
a.zipfiles,
|
||||||
|
a.datas,
|
||||||
|
[],
|
||||||
|
name='video2gif',
|
||||||
|
debug=False,
|
||||||
|
bootloader_ignore_signals=False,
|
||||||
|
strip=False,
|
||||||
|
upx=True,
|
||||||
|
upx_exclude=[],
|
||||||
|
runtime_tmpdir=None,
|
||||||
|
console=False,
|
||||||
|
disable_windowed_traceback=False,
|
||||||
|
target_arch=None,
|
||||||
|
codesign_identity=None,
|
||||||
|
entitlements_file=None,
|
||||||
|
)
|
Loading…
x
Reference in New Issue
Block a user