fix:初始化仓库
This commit is contained in:
304
douyin_video_crawler.py
Normal file
304
douyin_video_crawler.py
Normal file
@@ -0,0 +1,304 @@
|
||||
import time
|
||||
import os
|
||||
import requests
|
||||
import sys
|
||||
from DrissionPage import ChromiumOptions, ChromiumPage
|
||||
|
||||
|
||||
class VideoDownloader:
|
||||
"""视频下载器类"""
|
||||
|
||||
def __init__(self, video_dir):
|
||||
"""
|
||||
初始化视频下载器
|
||||
|
||||
Args:
|
||||
video_dir: 视频保存目录
|
||||
"""
|
||||
self.video_dir = video_dir
|
||||
|
||||
def save_video(self, video_url, aweme_id, video_title):
|
||||
"""
|
||||
下载视频到本地
|
||||
|
||||
Args:
|
||||
video_url: 视频下载地址
|
||||
aweme_id: 视频的唯一标识符
|
||||
video_title: 视频标题
|
||||
|
||||
Returns:
|
||||
bool: 下载是否成功
|
||||
"""
|
||||
try:
|
||||
# 清理视频标题,移除非法字符
|
||||
def clean_filename(filename):
|
||||
invalid_chars = '<>:"/\\|?*'
|
||||
for char in invalid_chars:
|
||||
filename = filename.replace(char, '_')
|
||||
return filename[:50] # 限制文件名长度
|
||||
|
||||
cleaned_title = clean_filename(video_title)
|
||||
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36...',
|
||||
'Referer': 'https://www.douyin.com/'
|
||||
}
|
||||
|
||||
print(f"开始下载视频: {cleaned_title}")
|
||||
print(f"视频ID: {aweme_id}")
|
||||
print(f"下载地址: {video_url}")
|
||||
|
||||
response = requests.get(video_url, headers=headers, stream=True, timeout=30)
|
||||
|
||||
if response.status_code == 200:
|
||||
video_path = os.path.join(self.video_dir, f'{cleaned_title}_{aweme_id}.mp4')
|
||||
print(f"保存路径: {video_path}")
|
||||
|
||||
total_size = int(response.headers.get('content-length', 0))
|
||||
downloaded_size = 0
|
||||
|
||||
with open(video_path, 'wb') as f:
|
||||
for chunk in response.iter_content(chunk_size=1024*1024):
|
||||
if chunk:
|
||||
f.write(chunk)
|
||||
downloaded_size += len(chunk)
|
||||
if total_size > 0:
|
||||
progress = (downloaded_size / total_size) * 100
|
||||
print(f"下载进度: {progress:.1f}%", end='\r')
|
||||
|
||||
print(f"\n视频下载完成: {cleaned_title}")
|
||||
|
||||
wait_time = time.time() % 2 + 1
|
||||
print(f"等待 {wait_time:.1f} 秒后继续...")
|
||||
time.sleep(wait_time)
|
||||
return True
|
||||
else:
|
||||
print(f"下载失败,状态码: {response.status_code}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"下载视频时出错: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
class VideoInfoExtractor:
|
||||
"""视频信息提取器类"""
|
||||
|
||||
def __init__(self, video_dir):
|
||||
"""
|
||||
初始化视频信息提取器
|
||||
|
||||
Args:
|
||||
video_dir: 视频保存目录
|
||||
"""
|
||||
self.downloader = VideoDownloader(video_dir)
|
||||
|
||||
def save_video_info(self, video_data):
|
||||
"""
|
||||
提取视频信息并下载视频
|
||||
|
||||
Args:
|
||||
video_data: 包含视频信息的字典
|
||||
|
||||
Returns:
|
||||
dict: 提取的视频信息字典
|
||||
"""
|
||||
print("\n开始提取视频信息...")
|
||||
|
||||
minutes = video_data['video']['duration'] // 1000 // 60
|
||||
seconds = video_data['video']['duration'] // 1000 % 60
|
||||
|
||||
video_url = video_data['video']['play_addr']['url_list'][0].replace('playwm', 'play')
|
||||
video_title = video_data['desc'].strip().replace('\n', '')
|
||||
aweme_id = video_data['aweme_id']
|
||||
|
||||
print(f"视频标题: {video_title}")
|
||||
print(f"视频时长: {minutes}:{seconds:02d}")
|
||||
print(f"作者: {video_data['author']['nickname'].strip()}")
|
||||
print(f"粉丝数: {video_data['author']['follower_count']}")
|
||||
print(f"点赞数: {video_data['statistics']['digg_count']}")
|
||||
|
||||
video_dict = {
|
||||
'用户名': video_data['author']['nickname'].strip(),
|
||||
'用户uid': 'a' + str(video_data['author']['uid']),
|
||||
'粉丝数量': video_data['author']['follower_count'],
|
||||
'视频描述': video_title,
|
||||
'视频标题': video_title,
|
||||
'点赞数量': video_data['statistics']['digg_count'],
|
||||
'视频awemeid': aweme_id,
|
||||
'视频时长': f"{minutes}:{seconds:02d}",
|
||||
'视频链接': video_url,
|
||||
}
|
||||
|
||||
print("开始下载视频...")
|
||||
download_success = self.downloader.save_video(video_url, aweme_id, video_title)
|
||||
|
||||
if download_success:
|
||||
print("视频信息提取和下载完成!")
|
||||
else:
|
||||
print("视频下载失败,但信息已提取")
|
||||
|
||||
return video_dict
|
||||
|
||||
|
||||
class DouyinCrawler:
|
||||
"""抖音爬虫类"""
|
||||
|
||||
def __init__(self, browser_path=None):
|
||||
"""
|
||||
初始化抖音爬虫
|
||||
|
||||
Args:
|
||||
browser_path: 浏览器路径,默认为 Edge 默认路径
|
||||
"""
|
||||
if browser_path is None:
|
||||
browser_path = '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge'
|
||||
self.browser_path = browser_path
|
||||
self.driver = None
|
||||
|
||||
def setup_browser(self):
|
||||
"""配置并初始化浏览器"""
|
||||
co = ChromiumOptions().set_browser_path(self.browser_path)
|
||||
self.driver = ChromiumPage(co)
|
||||
|
||||
def crawl_videos(self, keyword, video_dir):
|
||||
"""
|
||||
爬取视频数据
|
||||
|
||||
Args:
|
||||
keyword: 搜索关键词
|
||||
video_dir: 视频保存目录
|
||||
"""
|
||||
print(f"\n=== 开始爬取关键词: {keyword} ===")
|
||||
print(f"目标保存目录: {video_dir}")
|
||||
|
||||
extractor = VideoInfoExtractor(video_dir)
|
||||
|
||||
print("设置网络请求监听器...")
|
||||
self.driver.listen.start('www.douyin.com/aweme/v1/web/search/item', method='GET')
|
||||
|
||||
url = f'https://www.douyin.com/search/{keyword}?type=video'
|
||||
print(f"打开搜索页面: {url}")
|
||||
self.driver.get(url)
|
||||
|
||||
data_list = []
|
||||
total_videos = 0
|
||||
|
||||
print("\n开始爬取视频数据...")
|
||||
for page in range(10):
|
||||
print(f"\n=== 第 {page + 1} 页 ===")
|
||||
print("滚动到页面底部...")
|
||||
self.driver.scroll.to_bottom()
|
||||
|
||||
print("等待网络响应...")
|
||||
resp = self.driver.listen.wait()
|
||||
json_data = resp.response.body
|
||||
|
||||
print(f"获取到 {len(json_data['data'])} 个视频数据")
|
||||
|
||||
for json_aweme_info in json_data['data']:
|
||||
data = extractor.save_video_info(json_aweme_info['aweme_info'])
|
||||
data_list.append(data)
|
||||
total_videos += 1
|
||||
|
||||
print(f"当前累计爬取: {total_videos} 个视频")
|
||||
|
||||
if not json_data['has_more']:
|
||||
print("已到达最后一页,停止爬取")
|
||||
break
|
||||
|
||||
wait_time = time.time() % 3 + 2
|
||||
print(f"等待 {wait_time:.1f} 秒后继续下一页爬取...")
|
||||
time.sleep(wait_time)
|
||||
|
||||
print(f"\n=== 爬取完成 ===")
|
||||
print(f"总计爬取: {total_videos} 个视频")
|
||||
|
||||
# 保存元数据
|
||||
if data_list:
|
||||
VideoManager.save_metadata(data_list, video_dir)
|
||||
else:
|
||||
print("没有爬取到视频数据,跳过元数据保存")
|
||||
|
||||
def close(self):
|
||||
"""关闭浏览器"""
|
||||
if self.driver:
|
||||
self.driver.quit()
|
||||
|
||||
|
||||
class VideoManager:
|
||||
"""视频管理器类"""
|
||||
|
||||
@staticmethod
|
||||
def create_video_directory(keyword):
|
||||
"""
|
||||
创建视频保存目录
|
||||
|
||||
Args:
|
||||
keyword: 搜索关键词
|
||||
|
||||
Returns:
|
||||
str: 视频保存目录路径
|
||||
"""
|
||||
video_dir = f"./douyin_videos/{keyword}"
|
||||
if not os.path.exists(video_dir):
|
||||
print(f"创建视频保存目录: {video_dir}")
|
||||
os.makedirs(video_dir)
|
||||
else:
|
||||
print(f"使用现有目录: {video_dir}")
|
||||
return video_dir
|
||||
|
||||
@staticmethod
|
||||
def save_metadata(metadata_list, video_dir):
|
||||
"""
|
||||
保存视频元数据到JSON文件
|
||||
|
||||
Args:
|
||||
metadata_list: 视频元数据列表
|
||||
video_dir: 保存目录路径
|
||||
"""
|
||||
import json
|
||||
|
||||
metadata_file = os.path.join(video_dir, 'metaData.json')
|
||||
print(f"\n开始保存元数据到: {metadata_file}")
|
||||
print(f"共 {len(metadata_list)} 个视频的元数据")
|
||||
|
||||
try:
|
||||
# 添加保存时间
|
||||
metadata = {
|
||||
'save_time': time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()),
|
||||
'video_count': len(metadata_list),
|
||||
'videos': metadata_list
|
||||
}
|
||||
|
||||
with open(metadata_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(metadata, f, ensure_ascii=False, indent=2)
|
||||
|
||||
print(f"元数据保存成功!")
|
||||
print(f"文件大小: {os.path.getsize(metadata_file) / 1024:.2f} KB")
|
||||
except Exception as e:
|
||||
print(f"保存元数据时出错: {str(e)}")
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
if len(sys.argv) < 2:
|
||||
print("请提供关键词,例如:python pyauto.py 猫咪")
|
||||
sys.exit(1)
|
||||
|
||||
keyword = sys.argv[1]
|
||||
|
||||
# 创建视频保存目录
|
||||
video_dir = VideoManager.create_video_directory(keyword)
|
||||
|
||||
# 创建爬虫实例并开始爬取
|
||||
crawler = DouyinCrawler()
|
||||
|
||||
try:
|
||||
crawler.setup_browser()
|
||||
crawler.crawl_videos(keyword, video_dir)
|
||||
finally:
|
||||
crawler.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
524
video_converter_ui.py
Normal file
524
video_converter_ui.py
Normal file
@@ -0,0 +1,524 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
视频格式转换工具(带GUI界面)
|
||||
使用ffmpeg将指定目录下的全部视频转换为mp4格式的横屏1080p视频
|
||||
支持配置ffmpeg的大部分参数
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import tkinter as tk
|
||||
from tkinter import filedialog, messagebox, ttk
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
|
||||
|
||||
class VideoConverterUI:
|
||||
def __init__(self, root):
|
||||
self.root = root
|
||||
self.root.title("视频格式转换工具")
|
||||
self.root.geometry("800x800")
|
||||
self.root.resizable(True, True)
|
||||
|
||||
# 设置主题颜色
|
||||
self.bg_color = "#f0f0f0" # 主窗口背景色(浅灰)
|
||||
self.accent_color = "#4CAF50" # 强调色:标题、悬停边框(绿色)
|
||||
self.text_color = "#000000" # 普通文字颜色(黑色)
|
||||
self.frame_color = "#ffffff" # 内部框架背景色(白色)
|
||||
self.button_bg = "#4CAF50" # 按钮背景色(绿色)
|
||||
self.button_fg = "#000000" # 按钮文字颜色(白色)
|
||||
|
||||
# 变量
|
||||
self.input_dir = tk.StringVar()
|
||||
self.output_dir = tk.StringVar(value="output")
|
||||
self.threads = tk.StringVar(value="4")
|
||||
self.ffmpeg_params = tk.StringVar()
|
||||
self.is_converting = False
|
||||
self.convert_thread = None
|
||||
|
||||
# 创建界面
|
||||
self.create_widgets()
|
||||
|
||||
def create_widgets(self):
|
||||
# 设置窗口背景
|
||||
self.root.configure(bg=self.bg_color)
|
||||
|
||||
# 主框架
|
||||
main_frame = ttk.Frame(self.root, padding="20", style="Main.TFrame")
|
||||
main_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# 设置样式
|
||||
self.setup_styles()
|
||||
|
||||
# 标题
|
||||
title_label = ttk.Label(main_frame, text="视频格式转换工具", font=("Microsoft YaHei", 18, "bold"), style="Title.TLabel")
|
||||
title_label.pack(pady=10)
|
||||
|
||||
# 输入目录
|
||||
input_frame = ttk.LabelFrame(main_frame, text="输入设置", padding="15")
|
||||
input_frame.pack(fill=tk.X, pady=10)
|
||||
|
||||
# 输入目录行
|
||||
input_row = ttk.Frame(input_frame)
|
||||
input_row.pack(fill=tk.X, pady=8)
|
||||
ttk.Label(input_row, text="输入目录:", width=12, style="TLabel").pack(side=tk.LEFT, padx=5)
|
||||
|
||||
# 创建可拖拽的输入框
|
||||
self.input_entry = ttk.Entry(input_row, textvariable=self.input_dir, width=60, style="TEntry")
|
||||
self.input_entry.pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True)
|
||||
|
||||
# 启用拖放功能
|
||||
self.input_entry.bind("<Button-1>", self.clear_placeholder)
|
||||
self.input_entry.bind("<Enter>", self.on_enter)
|
||||
self.input_entry.bind("<Leave>", self.on_leave)
|
||||
|
||||
# 为输入框添加拖放支持
|
||||
self.input_entry.bind("<Button-3>", self.on_right_click)
|
||||
|
||||
# 为窗口添加拖放支持(使用tkinter标准事件)
|
||||
self.root.bind("<ButtonPress-1>", self.on_button_press)
|
||||
self.root.bind("<B1-Motion>", self.on_mouse_move)
|
||||
self.root.bind("<ButtonRelease-1>", self.on_button_release)
|
||||
|
||||
# 为输入框添加粘贴支持
|
||||
self.input_entry.bind("<Button-2>", self.on_paste)
|
||||
self.input_entry.bind("<Control-v>", self.on_paste)
|
||||
|
||||
ttk.Button(input_row, text="浏览", command=self.browse_input, style="Accent.TButton", width=8).pack(side=tk.RIGHT, padx=5)
|
||||
|
||||
# 输出目录行
|
||||
output_row = ttk.Frame(input_frame)
|
||||
output_row.pack(fill=tk.X, pady=8)
|
||||
ttk.Label(output_row, text="输出目录:", width=12, style="TLabel").pack(side=tk.LEFT, padx=5)
|
||||
ttk.Entry(output_row, textvariable=self.output_dir, width=60, style="TEntry").pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True)
|
||||
ttk.Button(output_row, text="浏览", command=self.browse_output, style="Accent.TButton", width=8).pack(side=tk.RIGHT, padx=5)
|
||||
|
||||
# 线程数和参数行
|
||||
settings_row = ttk.Frame(input_frame)
|
||||
settings_row.pack(fill=tk.X, pady=8)
|
||||
|
||||
threads_frame = ttk.Frame(settings_row)
|
||||
threads_frame.pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True)
|
||||
ttk.Label(threads_frame, text="并发线程数:", width=12, style="TLabel").pack(side=tk.LEFT, padx=5)
|
||||
ttk.Entry(threads_frame, textvariable=self.threads, width=10, style="TEntry").pack(side=tk.LEFT, padx=5)
|
||||
|
||||
# 分辨率设置按钮
|
||||
resolution_frame = ttk.LabelFrame(main_frame, text="分辨率设置", padding="15")
|
||||
resolution_frame.pack(fill=tk.X, pady=10)
|
||||
|
||||
buttons_frame = ttk.Frame(resolution_frame)
|
||||
buttons_frame.pack(fill=tk.X, pady=5)
|
||||
|
||||
ttk.Button(buttons_frame, text="强制高度为1080", command=self.set_height_1080, style="Accent.TButton").pack(side=tk.LEFT, padx=10, ipady=3, ipadx=15)
|
||||
ttk.Button(buttons_frame, text="强制宽度为1920", command=self.set_width_1920, style="Accent.TButton").pack(side=tk.LEFT, padx=10, ipady=3, ipadx=15)
|
||||
ttk.Button(buttons_frame, text="严格1080p(首选)", command=self.set_strict_1080p, style="Accent.TButton").pack(side=tk.LEFT, padx=10, ipady=3, ipadx=15)
|
||||
|
||||
# FFmpeg参数设置
|
||||
params_frame = ttk.LabelFrame(main_frame, text="FFmpeg参数设置", padding="15")
|
||||
params_frame.pack(fill=tk.X, pady=10)
|
||||
|
||||
params_row = ttk.Frame(params_frame)
|
||||
params_row.pack(fill=tk.X, pady=5)
|
||||
ttk.Label(params_row, text="额外参数:", width=12, style="TLabel").pack(side=tk.LEFT, padx=5)
|
||||
ttk.Entry(params_row, textvariable=self.ffmpeg_params, width=60, style="TEntry").pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True)
|
||||
|
||||
# 控制按钮
|
||||
button_frame = ttk.Frame(main_frame)
|
||||
button_frame.pack(fill=tk.X, pady=15)
|
||||
|
||||
button_container = ttk.Frame(button_frame)
|
||||
button_container.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
self.convert_button = ttk.Button(button_container, text="开始转换", command=self.start_conversion, style="Start.TButton")
|
||||
self.convert_button.pack(side=tk.LEFT, padx=10, ipady=10, ipadx=25)
|
||||
|
||||
self.stop_button = ttk.Button(button_container, text="停止转换", command=self.stop_conversion, state=tk.DISABLED, style="Stop.TButton")
|
||||
self.stop_button.pack(side=tk.LEFT, padx=10, ipady=10, ipadx=25)
|
||||
|
||||
# 状态框
|
||||
status_frame = ttk.LabelFrame(main_frame, text="转换状态", padding="15")
|
||||
status_frame.pack(fill=tk.BOTH, expand=True, pady=10)
|
||||
|
||||
# 状态文本框
|
||||
self.status_text = tk.Text(status_frame, wrap=tk.WORD, height=15, bg="#f8f8f8", fg=self.text_color, font=("Microsoft YaHei", 10))
|
||||
self.status_text.pack(fill=tk.BOTH, expand=True, side=tk.LEFT, padx=5, pady=5)
|
||||
|
||||
# 滚动条
|
||||
scrollbar = ttk.Scrollbar(status_frame, command=self.status_text.yview, style="TScrollbar")
|
||||
scrollbar.pack(side=tk.RIGHT, fill=tk.Y, pady=5)
|
||||
self.status_text.config(yscrollcommand=scrollbar.set)
|
||||
|
||||
# 底部状态
|
||||
self.status_var = tk.StringVar(value="就绪")
|
||||
status_bar = ttk.Label(self.root, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W, style="Status.TLabel")
|
||||
status_bar.pack(fill=tk.X, side=tk.BOTTOM)
|
||||
|
||||
def setup_styles(self):
|
||||
"""设置界面样式"""
|
||||
style = ttk.Style()
|
||||
|
||||
# 主框架样式
|
||||
style.configure("Main.TFrame", background=self.bg_color)
|
||||
|
||||
# 标题样式
|
||||
style.configure("Title.TLabel", font=("Microsoft YaHei", 18, "bold"), foreground=self.accent_color, background=self.bg_color)
|
||||
|
||||
# 标签样式
|
||||
style.configure("TLabel", font=("Microsoft YaHei", 10, "bold"), foreground=self.text_color, background=self.frame_color)
|
||||
|
||||
# 输入框样式
|
||||
style.configure("TEntry", font=("Microsoft YaHei", 10), padding=5, foreground=self.text_color)
|
||||
# 悬停输入框样式
|
||||
style.configure("Hover.TEntry", font=("Microsoft YaHei", 10), padding=5, foreground=self.accent_color, background="#f0f8f0")
|
||||
|
||||
# 按钮样式
|
||||
style.configure("Accent.TButton", font=("Microsoft YaHei", 10), padding=5, background=self.button_bg, foreground=self.button_fg)
|
||||
style.map("Accent.TButton", background=[("active", "#45a049")])
|
||||
|
||||
# 开始按钮样式
|
||||
style.configure("Start.TButton", font=("Microsoft YaHei", 11, "bold"), padding=8, background=self.button_bg, foreground=self.button_fg)
|
||||
style.map("Start.TButton", background=[("active", "#45a049")])
|
||||
|
||||
# 停止按钮样式
|
||||
style.configure("Stop.TButton", font=("Microsoft YaHei", 11, "bold"), padding=8, background="#f44336", foreground="#ffffff")
|
||||
style.map("Stop.TButton", background=[("active", "#da190b")])
|
||||
|
||||
# 标签框架样式
|
||||
style.configure("TLabelFrame", font=("Microsoft YaHei", 10, "bold"), background=self.bg_color, foreground=self.text_color)
|
||||
style.configure("TLabelFrame.Label", font=("Microsoft YaHei", 10, "bold"), foreground=self.text_color)
|
||||
|
||||
# 滚动条样式
|
||||
style.configure("TScrollbar", background=self.bg_color, troughcolor="#e0e0e0")
|
||||
|
||||
# 状态标签样式
|
||||
style.configure("Status.TLabel", font=("Microsoft YaHei", 9), padding=5, background="#e0e0e0", foreground=self.text_color)
|
||||
|
||||
def browse_input(self):
|
||||
dir_path = filedialog.askdirectory(title="选择输入目录")
|
||||
if dir_path:
|
||||
self.input_dir.set(dir_path)
|
||||
|
||||
def browse_output(self):
|
||||
dir_path = filedialog.askdirectory(title="选择输出目录")
|
||||
if dir_path:
|
||||
self.output_dir.set(dir_path)
|
||||
|
||||
def set_height_1080(self):
|
||||
"""设置高度为1080"""
|
||||
current_params = self.ffmpeg_params.get().strip()
|
||||
# 移除现有的高度设置
|
||||
new_params = []
|
||||
for param in current_params.split():
|
||||
if not (param.startswith('-h') and param != '-h'):
|
||||
new_params.append(param)
|
||||
# 添加新的高度设置
|
||||
new_params.extend(['-vf', 'scale=-2:1080'])
|
||||
self.ffmpeg_params.set(' '.join(new_params))
|
||||
self.log("已设置高度为1080")
|
||||
|
||||
def set_width_1920(self):
|
||||
"""设置宽度为1920"""
|
||||
current_params = self.ffmpeg_params.get().strip()
|
||||
# 移除现有的宽度设置
|
||||
new_params = []
|
||||
for param in current_params.split():
|
||||
if not (param.startswith('-w') and param != '-w'):
|
||||
new_params.append(param)
|
||||
# 添加新的宽度设置
|
||||
new_params.extend(['-vf', 'scale=1920:-2'])
|
||||
self.ffmpeg_params.set(' '.join(new_params))
|
||||
self.log("已设置宽度为1920")
|
||||
|
||||
def set_strict_1080p(self):
|
||||
"""设置严格的1080p分辨率"""
|
||||
current_params = self.ffmpeg_params.get().strip()
|
||||
# 移除现有的分辨率设置
|
||||
new_params = []
|
||||
skip_next = False
|
||||
for i, param in enumerate(current_params.split()):
|
||||
if skip_next:
|
||||
skip_next = False
|
||||
continue
|
||||
if param == '-vf' and i + 1 < len(current_params.split()):
|
||||
if 'scale=' in current_params.split()[i + 1]:
|
||||
skip_next = True
|
||||
continue
|
||||
new_params.append(param)
|
||||
# 添加严格的1080p设置(先缩放保持比例,再用pad填充黑边至标准1080p)
|
||||
new_params.extend(['-vf', 'scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2'])
|
||||
self.ffmpeg_params.set(' '.join(new_params))
|
||||
self.log("已设置严格的1080p分辨率(保持比例+黑边填充)")
|
||||
|
||||
def clear_placeholder(self, event):
|
||||
"""清除输入框的占位符"""
|
||||
pass
|
||||
|
||||
def on_enter(self, event):
|
||||
"""鼠标进入输入框"""
|
||||
self.input_entry.configure(style="Hover.TEntry")
|
||||
|
||||
def on_leave(self, event):
|
||||
"""鼠标离开输入框"""
|
||||
self.input_entry.configure(style="TEntry")
|
||||
|
||||
def on_button_press(self, event):
|
||||
"""鼠标按下"""
|
||||
pass
|
||||
|
||||
def on_mouse_move(self, event):
|
||||
"""鼠标移动"""
|
||||
pass
|
||||
|
||||
def on_button_release(self, event):
|
||||
"""鼠标释放"""
|
||||
pass
|
||||
|
||||
def on_right_click(self, event):
|
||||
"""右键点击"""
|
||||
pass
|
||||
|
||||
def on_paste(self, event):
|
||||
"""粘贴功能"""
|
||||
try:
|
||||
# 获取剪贴板内容
|
||||
clipboard_content = self.root.clipboard_get()
|
||||
if clipboard_content:
|
||||
# 检查是否是有效的文件或目录路径
|
||||
if os.path.exists(clipboard_content):
|
||||
# 如果是文件,使用其所在目录
|
||||
if os.path.isfile(clipboard_content):
|
||||
input_path = os.path.dirname(clipboard_content)
|
||||
else:
|
||||
input_path = clipboard_content
|
||||
self.input_dir.set(input_path)
|
||||
self.log(f"已添加输入目录: {input_path}")
|
||||
except:
|
||||
pass
|
||||
|
||||
def log(self, message):
|
||||
"""记录日志到状态文本框"""
|
||||
self.status_text.insert(tk.END, message + "\n")
|
||||
self.status_text.see(tk.END)
|
||||
self.root.update_idletasks()
|
||||
|
||||
def get_video_files(self, directory):
|
||||
"""
|
||||
获取目录下所有视频文件
|
||||
"""
|
||||
video_extensions = ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv', '.webm', '.m4v']
|
||||
video_files = []
|
||||
|
||||
for root, _, files in os.walk(directory):
|
||||
for file in files:
|
||||
if any(file.lower().endswith(ext) for ext in video_extensions):
|
||||
video_files.append(os.path.join(root, file))
|
||||
|
||||
return video_files
|
||||
|
||||
def convert_video(self, input_file, output_file, ffmpeg_args):
|
||||
"""
|
||||
转换单个视频文件
|
||||
"""
|
||||
try:
|
||||
# 构建ffmpeg命令
|
||||
cmd = [
|
||||
'ffmpeg', '-i', input_file,
|
||||
'-c:v', 'libx264', '-preset', 'medium', '-crf', '23',
|
||||
'-c:a', 'aac', '-b:a', '128k',
|
||||
'-y' # 覆盖输出文件
|
||||
]
|
||||
|
||||
# 检查用户是否设置了分辨率参数
|
||||
has_resolution_param = False
|
||||
for i, arg in enumerate(ffmpeg_args):
|
||||
if arg == '-vf' and i + 1 < len(ffmpeg_args):
|
||||
if 'scale=' in ffmpeg_args[i + 1]:
|
||||
has_resolution_param = True
|
||||
break
|
||||
|
||||
# 如果用户没有设置分辨率,使用默认的1080p横屏设置
|
||||
if not has_resolution_param:
|
||||
cmd.extend(['-vf', 'scale=1920:1080,setdar=16:9']) # 默认强制横屏1080p
|
||||
|
||||
# 添加用户自定义参数
|
||||
cmd.extend(ffmpeg_args)
|
||||
cmd.append(output_file)
|
||||
|
||||
# 执行命令
|
||||
self.log(f"正在转换: {os.path.basename(input_file)}")
|
||||
subprocess.run(cmd, check=True, capture_output=True, text=True)
|
||||
self.log(f"转换完成: {os.path.basename(output_file)}")
|
||||
return True
|
||||
except subprocess.CalledProcessError as e:
|
||||
self.log(f"转换失败: {os.path.basename(input_file)}")
|
||||
self.log(f"错误信息: {e.stderr}")
|
||||
return False
|
||||
except Exception as e:
|
||||
self.log(f"转换失败: {os.path.basename(input_file)}")
|
||||
self.log(f"错误信息: {str(e)}")
|
||||
return False
|
||||
|
||||
def start_conversion(self):
|
||||
"""开始转换过程"""
|
||||
# 验证输入
|
||||
input_dir = self.input_dir.get().strip()
|
||||
if not input_dir:
|
||||
messagebox.showerror("错误", "请选择输入目录")
|
||||
return
|
||||
|
||||
if not os.path.exists(input_dir):
|
||||
messagebox.showerror("错误", f"输入目录 '{input_dir}' 不存在")
|
||||
return
|
||||
|
||||
output_dir = self.output_dir.get().strip()
|
||||
if not output_dir:
|
||||
messagebox.showerror("错误", "请选择输出目录")
|
||||
return
|
||||
|
||||
try:
|
||||
threads = int(self.threads.get().strip())
|
||||
if threads <= 0:
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
messagebox.showerror("错误", "并发线程数必须是正整数")
|
||||
return
|
||||
|
||||
# 解析FFmpeg参数
|
||||
ffmpeg_params = self.ffmpeg_params.get().strip()
|
||||
ffmpeg_args = ffmpeg_params.split() if ffmpeg_params else []
|
||||
|
||||
# 更新UI状态
|
||||
self.is_converting = True
|
||||
self.convert_button.config(state=tk.DISABLED)
|
||||
self.stop_button.config(state=tk.NORMAL)
|
||||
self.status_var.set("转换中...")
|
||||
self.status_text.delete(1.0, tk.END)
|
||||
|
||||
# 启动转换线程
|
||||
def conversion_thread():
|
||||
try:
|
||||
# 创建输出目录
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
# 获取所有视频文件
|
||||
video_files = self.get_video_files(input_dir)
|
||||
|
||||
if not video_files:
|
||||
self.log(f"错误: 输入目录 '{input_dir}' 中没有找到视频文件")
|
||||
return
|
||||
|
||||
self.log(f"找到 {len(video_files)} 个视频文件")
|
||||
|
||||
# 并发转换视频
|
||||
success_count = 0
|
||||
failure_count = 0
|
||||
|
||||
with ThreadPoolExecutor(max_workers=threads) as executor:
|
||||
future_to_video = {}
|
||||
|
||||
for video_file in video_files:
|
||||
if not self.is_converting:
|
||||
break
|
||||
|
||||
# 构建输出文件路径
|
||||
relative_path = os.path.relpath(video_file, input_dir)
|
||||
output_file = os.path.join(output_dir, os.path.splitext(relative_path)[0] + '.mp4')
|
||||
|
||||
# 创建输出文件的目录
|
||||
os.makedirs(os.path.dirname(output_file), exist_ok=True)
|
||||
|
||||
# 提交任务
|
||||
future = executor.submit(self.convert_video, video_file, output_file, ffmpeg_args)
|
||||
future_to_video[future] = video_file
|
||||
|
||||
# 处理结果
|
||||
for future in as_completed(future_to_video):
|
||||
if not self.is_converting:
|
||||
# 取消所有未完成的任务
|
||||
for f in future_to_video:
|
||||
if not f.done():
|
||||
f.cancel()
|
||||
break
|
||||
|
||||
video_file = future_to_video[future]
|
||||
try:
|
||||
result = future.result()
|
||||
if result:
|
||||
success_count += 1
|
||||
else:
|
||||
failure_count += 1
|
||||
except Exception as e:
|
||||
self.log(f"处理 {os.path.basename(video_file)} 时出错: {str(e)}")
|
||||
failure_count += 1
|
||||
|
||||
if self.is_converting:
|
||||
self.log(f"\n转换完成:")
|
||||
self.log(f"成功: {success_count}")
|
||||
self.log(f"失败: {failure_count}")
|
||||
messagebox.showinfo("转换完成", f"转换完成:\n成功: {success_count}\n失败: {failure_count}")
|
||||
else:
|
||||
self.log("转换已停止")
|
||||
messagebox.showinfo("转换停止", "转换已停止")
|
||||
except Exception as e:
|
||||
self.log(f"转换过程出错: {str(e)}")
|
||||
messagebox.showerror("错误", f"转换过程出错: {str(e)}")
|
||||
finally:
|
||||
# 恢复UI状态
|
||||
self.is_converting = False
|
||||
self.convert_button.config(state=tk.NORMAL)
|
||||
self.stop_button.config(state=tk.DISABLED)
|
||||
self.status_var.set("就绪")
|
||||
|
||||
self.convert_thread = threading.Thread(target=conversion_thread)
|
||||
self.convert_thread.daemon = True
|
||||
self.convert_thread.start()
|
||||
|
||||
def stop_conversion(self):
|
||||
"""停止转换过程"""
|
||||
if messagebox.askyesno("确认", "确定要停止转换吗?"):
|
||||
self.is_converting = False
|
||||
self.status_var.set("停止中...")
|
||||
|
||||
# 结束所有ffmpeg进程
|
||||
try:
|
||||
import psutil
|
||||
for proc in psutil.process_iter(['pid', 'name']):
|
||||
if proc.info['name'] and 'ffmpeg' in proc.info['name'].lower():
|
||||
proc.kill()
|
||||
self.log(f"已结束ffmpeg进程 (PID: {proc.info['pid']})")
|
||||
except ImportError:
|
||||
# 如果没有psutil,使用subprocess
|
||||
try:
|
||||
subprocess.run(['taskkill', '/F', '/IM', 'ffmpeg.exe'], capture_output=True)
|
||||
self.log("已结束所有ffmpeg进程")
|
||||
except:
|
||||
self.log("结束ffmpeg进程失败,请手动结束")
|
||||
except Exception as e:
|
||||
self.log(f"结束ffmpeg进程时出错: {str(e)}")
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
主函数
|
||||
"""
|
||||
# 检查ffmpeg是否存在
|
||||
if not os.path.exists('ffmpeg.exe'):
|
||||
# 尝试在系统路径中查找
|
||||
try:
|
||||
subprocess.run(['ffmpeg', '-version'], capture_output=True, check=True)
|
||||
except:
|
||||
messagebox.showerror("错误", "未找到ffmpeg.exe,请确保它与程序在同一目录下")
|
||||
return
|
||||
|
||||
# 创建主窗口
|
||||
root = tk.Tk()
|
||||
app = VideoConverterUI(root)
|
||||
|
||||
# 运行主循环
|
||||
root.mainloop()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user