youtube-clipper / app.py
yuchen-xiyue
Add cookies upload choice
a48f1c6
Raw
History Blame Contribute Delete
26.3 kB
import gradio as gr
import yt_dlp
import os
import re
import time
import json
import subprocess
from datetime import datetime
# Add imports for the other engines
from pytube import YouTube as PytubeYouTube
try:
from pytubefix import YouTube as PytubefixYouTube
except ImportError:
PytubefixYouTube = None
def time_to_seconds(time_str):
if not time_str:
return 0
if re.match(r'^\d+$', time_str):
return int(time_str)
elif re.match(r'^\d+:\d+$', time_str):
minutes, seconds = time_str.split(':')
return int(minutes) * 60 + int(seconds)
elif re.match(r'^\d+:\d+:\d+$', time_str):
hours, minutes, seconds = time_str.split(':')
return int(hours) * 3600 + int(minutes) * 60 + int(seconds)
return 0
def seconds_to_time_str(seconds):
hours, remainder = divmod(seconds, 3600)
minutes, seconds = divmod(remainder, 60)
return f"{hours:02d}:{minutes:02d}:{seconds:02d}"
# ====== YT-DLP Implementation ======
def get_video_formats(info):
"""Extract available video formats from yt-dlp info"""
formats = []
for f in info.get('formats', []):
if f.get('resolution') != 'audio only' and f.get('vcodec') != 'none':
height = f.get('height')
if height:
formats.append({
'format_id': f.get('format_id'),
'resolution': f"{height}p",
'fps': f.get('fps'),
'vcodec': f.get('vcodec'),
'acodec': f.get('acodec'),
'ext': f.get('ext'),
'filesize': f.get('filesize'),
'has_audio': f.get('acodec') != 'none'
})
return formats
def fetch_video_info_ytdlp(video_url, cookies_file=None):
try:
ydl_opts = {
'quiet': True,
'no_warnings': True,
'skip_download': True,
}
# Add cookies file if provided
if cookies_file and os.path.exists(cookies_file):
ydl_opts['cookiefile'] = cookies_file
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
info = ydl.extract_info(video_url, download=False)
# Extract basic information
title = info.get('title', '')
length = info.get('duration', 0)
author = info.get('uploader', '')
views = info.get('view_count', 0)
description = info.get('description', '')
thumbnail = info.get('thumbnail', '')
duration_str = seconds_to_time_str(length)
formats = get_video_formats(info)
all_resolutions = set()
for f in formats:
if f.get('resolution'):
all_resolutions.add(f.get('resolution'))
qualities = sorted(list(all_resolutions),
key=lambda x: int(x.replace('p', '')))
print(f"Available qualities: {qualities}")
if not qualities:
qualities = ["360p", "480p", "720p", "1080p"]
default_quality = qualities[-1] if qualities else "1080p"
return "", title, author, f"{views:,}", duration_str, description[:500] + "..." if len(description) > 500 else description, thumbnail, gr.update(visible=True), gr.update(choices=qualities, value=default_quality)
except Exception as e:
return f"错误: {str(e)}", "", "", "", "", "", None, gr.update(visible=False), gr.update(choices=["360p", "480p", "720p", "1080p"], value="1080p")
def download_video_segment_ytdlp(video_url, start_time, end_time, quality, fps, progress=gr.Progress(), cookies_file=None):
try:
start_seconds = time_to_seconds(start_time)
end_seconds = time_to_seconds(end_time)
if end_seconds <= start_seconds:
return "结束时间必须大于开始时间"
# Extract video information
progress(0.1, "正在获取视频信息...")
ydl_opts = {
'quiet': True,
'no_warnings': True,
'skip_download': True,
}
# Add cookies file if provided
if cookies_file and os.path.exists(cookies_file):
ydl_opts['cookiefile'] = cookies_file
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
info = ydl.extract_info(video_url, download=False)
title = info.get('title', 'video')
safe_title = re.sub(r'[^\w\-_]', '_', title)
formats = get_video_formats(info)
resolution = quality
fps_value = int(fps.replace("fps", ""))
progress(0.3, f"正在选择视频流 (质量: {quality}, 帧率: {fps})...")
selected_format = None
for f in formats:
if f['resolution'] == quality and f['fps'] and f['fps'] >= fps_value and f['has_audio']:
selected_format = f
break
if not selected_format:
progress(0.3, "正在选择最佳视频和音频流...")
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
temp_video = f"temp_video_{timestamp}.mp4"
temp_audio = f"temp_audio_{timestamp}.mp4"
temp_output = f"temp_combined_{timestamp}.mp4"
output_filename = f"{safe_title}_{timestamp}_{start_time.replace(':', '')}_{end_time.replace(':', '')}.mp4"
progress(0.4, "正在下载视频...")
video_ydl_opts = {
'format': f'bestvideo[height<={quality.replace("p", "")}]',
'outtmpl': temp_video,
'quiet': True
}
# Add cookies file if provided
if cookies_file and os.path.exists(cookies_file):
video_ydl_opts['cookiefile'] = cookies_file
with yt_dlp.YoutubeDL(video_ydl_opts) as ydl:
ydl.download([video_url])
progress(0.5, "正在下载音频...")
audio_ydl_opts = {
'format': 'bestaudio',
'outtmpl': temp_audio,
'quiet': True
}
with yt_dlp.YoutubeDL(audio_ydl_opts) as ydl:
ydl.download([video_url])
progress(0.6, "正在合并视频和音频...")
output_file_temp = os.path.join(os.getcwd(), temp_output)
cmd_combine = [
'ffmpeg', '-y',
'-i', temp_video,
'-i', temp_audio,
'-c', 'copy',
output_file_temp
]
subprocess.run(cmd_combine, check=True)
progress(0.7, "正在剪辑视频片段...")
output_file = os.path.join(os.getcwd(), output_filename)
cmd_cut = [
'ffmpeg', '-y',
'-i', output_file_temp,
'-ss', str(start_seconds),
'-to', str(end_seconds),
'-c:v', 'copy',
'-c:a', 'copy',
output_file
]
subprocess.run(cmd_cut, check=True)
os.remove(temp_video)
os.remove(temp_audio)
os.remove(output_file_temp)
progress(1.0, "已完成!")
return output_file
format_id = selected_format['format_id']
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
temp_filename = f"temp_{timestamp}.mp4"
output_filename = f"{safe_title}_{timestamp}_{start_time.replace(':', '')}_{end_time.replace(':', '')}.mp4"
progress(0.4, "正在下载视频...")
ydl_opts = {
'format': format_id,
'outtmpl': temp_filename,
'quiet': True
}
# Add cookies file if provided
if cookies_file and os.path.exists(cookies_file):
ydl_opts['cookiefile'] = cookies_file
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
ydl.download([video_url])
progress(0.7, "正在剪辑视频片段...")
output_file = os.path.join(os.getcwd(), output_filename)
cmd = [
'ffmpeg', '-y',
'-i', temp_filename,
'-ss', str(start_seconds),
'-to', str(end_seconds),
'-c:v', 'copy',
'-c:a', 'copy',
output_file
]
subprocess.run(cmd, check=True)
os.remove(temp_filename)
progress(1.0, "已完成!")
return output_file
except Exception as e:
return f"错误: {str(e)}"
# ====== PyTube Implementation ======
def fetch_video_info_pytube(video_url):
try:
yt = PytubeYouTube(video_url)
title = yt.title
length = yt.length
author = yt.author
views = yt.views
description = yt.description
thumbnail = yt.thumbnail_url
duration_str = seconds_to_time_str(length)
progressive_streams = yt.streams.filter(progressive=True)
adaptive_streams = yt.streams.filter(adaptive=True, only_video=True)
all_resolutions = set()
for s in progressive_streams:
if s.resolution:
all_resolutions.add(s.resolution)
for s in adaptive_streams:
if s.resolution:
all_resolutions.add(s.resolution)
qualities = sorted(list(all_resolutions),
key=lambda x: int(x.replace('p', '')))
print(f"Available qualities: {qualities}")
if not qualities:
qualities = ["360p", "480p", "720p", "1080p"]
default_quality = qualities[-1] if qualities else "1080p"
return "", title, author, f"{views:,}", duration_str, description[:500] + "..." if len(description) > 500 else description, thumbnail, gr.update(visible=True), gr.update(choices=qualities, value=default_quality)
except Exception as e:
return f"错误: {str(e)}", "", "", "", "", "", None, gr.update(visible=False), gr.update(choices=["360p", "480p", "720p", "1080p"], value="1080p")
def download_video_segment_pytube(video_url, start_time, end_time, quality, fps, progress=gr.Progress()):
try:
start_seconds = time_to_seconds(start_time)
end_seconds = time_to_seconds(end_time)
if end_seconds <= start_seconds:
return "结束时间必须大于开始时间"
progress(0.1, "正在获取视频信息...")
yt = PytubeYouTube(video_url)
title = yt.title
safe_title = re.sub(r'[^\w\-_]', '_', title)
resolution = quality
fps_value = int(fps.replace("fps", ""))
progress(0.3, f"正在选择视频流 (质量: {quality}, 帧率: {fps})...")
streams = yt.streams.filter(progressive=True, res=quality)
if not streams:
print(f"No progressive stream found for {quality}, trying adaptive...")
video_streams = yt.streams.filter(adaptive=True, only_video=True, res=quality)
if video_streams:
video_stream = video_streams.first()
audio_stream = yt.streams.filter(only_audio=True).first()
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
temp_video = f"temp_video_{timestamp}.mp4"
temp_audio = f"temp_audio_{timestamp}.mp4"
temp_output = f"temp_combined_{timestamp}.mp4"
output_filename = f"{safe_title}_{timestamp}_{start_time.replace(':', '')}_{end_time.replace(':', '')}.mp4"
progress(0.4, "正在下载视频流...")
video_path = video_stream.download(filename=temp_video)
progress(0.5, "正在下载音频流...")
audio_path = audio_stream.download(filename=temp_audio)
progress(0.6, "正在合并视频和音频...")
output_file_temp = os.path.join(os.getcwd(), temp_output)
cmd_combine = [
'ffmpeg', '-y',
'-i', video_path,
'-i', audio_path,
'-c', 'copy',
output_file_temp
]
subprocess.run(cmd_combine, check=True)
progress(0.7, "正在剪辑视频片段...")
output_file = os.path.join(os.getcwd(), output_filename)
cmd_cut = [
'ffmpeg', '-y',
'-i', output_file_temp,
'-ss', str(start_seconds),
'-to', str(end_seconds),
'-c:v', 'copy',
'-c:a', 'copy',
output_file
]
subprocess.run(cmd_cut, check=True)
os.remove(video_path)
os.remove(audio_path)
os.remove(output_file_temp)
progress(1.0, "已完成!")
return output_file
else:
streams = yt.streams.filter(progressive=True).order_by('resolution').desc()
if not streams:
return "无法找到合适的视频流"
video_stream = streams.first()
print(f"Selected stream: {video_stream.resolution} {video_stream.fps}fps")
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
output_filename = f"{safe_title}_{timestamp}_{start_time.replace(':', '')}_{end_time.replace(':', '')}.mp4"
progress(0.4, "正在下载视频...")
download_path = video_stream.download(filename=f"temp_{output_filename}")
progress(0.7, "正在剪辑视频片段...")
output_file = os.path.join(os.getcwd(), output_filename)
cmd = [
'ffmpeg', '-y',
'-i', download_path,
'-ss', str(start_seconds),
'-to', str(end_seconds),
'-c:v', 'copy',
'-c:a', 'copy',
output_file
]
subprocess.run(cmd, check=True)
os.remove(download_path)
progress(1.0, "已完成!")
return output_file
except Exception as e:
return f"错误: {str(e)}"
# ====== PyTubeFix Implementation ======
def fetch_video_info_pytubefix(video_url, po_token="", visitor_data=""):
try:
if PytubefixYouTube is None:
return "错误: pytubefix 未安装", "", "", "", "", "", None, gr.update(visible=False), gr.update(choices=["360p", "480p", "720p", "1080p"], value="1080p")
# Create YouTube object with token verification if tokens are provided
if po_token and visitor_data:
yt = PytubefixYouTube(
video_url,
use_po_token=True,
po_token_verifier=lambda: (visitor_data, po_token)
)
else:
yt = PytubefixYouTube(video_url)
title = yt.title
length = yt.length
author = yt.author
views = yt.views
description = yt.description
thumbnail = yt.thumbnail_url
duration_str = seconds_to_time_str(length)
progressive_streams = yt.streams.filter(progressive=True)
adaptive_streams = yt.streams.filter(adaptive=True, only_video=True)
all_resolutions = set()
for s in progressive_streams:
if s.resolution:
all_resolutions.add(s.resolution)
for s in adaptive_streams:
if s.resolution:
all_resolutions.add(s.resolution)
qualities = sorted(list(all_resolutions),
key=lambda x: int(x.replace('p', '')))
print(f"Available qualities: {qualities}")
if not qualities:
qualities = ["360p", "480p", "720p", "1080p"]
default_quality = qualities[-1] if qualities else "1080p"
return "", title, author, f"{views:,}", duration_str, description[:500] + "..." if len(description) > 500 else description, thumbnail, gr.update(visible=True), gr.update(choices=qualities, value=default_quality)
except Exception as e:
return f"错误: {str(e)}", "", "", "", "", "", None, gr.update(visible=False), gr.update(choices=["360p", "480p", "720p", "1080p"], value="1080p")
def download_video_segment_pytubefix(video_url, start_time, end_time, quality, fps, progress=gr.Progress(), po_token="", visitor_data=""):
try:
if PytubefixYouTube is None:
return "错误: pytubefix 未安装"
start_seconds = time_to_seconds(start_time)
end_seconds = time_to_seconds(end_time)
if end_seconds <= start_seconds:
return "结束时间必须大于开始时间"
progress(0.1, "正在获取视频信息...")
# Create YouTube object with token verification if tokens are provided
if po_token and visitor_data:
yt = PytubefixYouTube(
video_url,
use_po_token=True,
po_token_verifier=lambda: (visitor_data, po_token)
)
else:
yt = PytubefixYouTube(video_url)
title = yt.title
safe_title = re.sub(r'[^\w\-_]', '_', title)
resolution = quality
fps_value = int(fps.replace("fps", ""))
progress(0.3, f"正在选择视频流 (质量: {quality}, 帧率: {fps})...")
streams = yt.streams.filter(progressive=True, res=quality)
if not streams:
print(f"No progressive stream found for {quality}, trying adaptive...")
video_streams = yt.streams.filter(adaptive=True, only_video=True, res=quality)
if video_streams:
video_stream = video_streams.first()
audio_stream = yt.streams.filter(only_audio=True).first()
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
temp_video = f"temp_video_{timestamp}.mp4"
temp_audio = f"temp_audio_{timestamp}.mp4"
temp_output = f"temp_combined_{timestamp}.mp4"
output_filename = f"{safe_title}_{timestamp}_{start_time.replace(':', '')}_{end_time.replace(':', '')}.mp4"
progress(0.4, "正在下载视频流...")
video_path = video_stream.download(filename=temp_video)
progress(0.5, "正在下载音频流...")
audio_path = audio_stream.download(filename=temp_audio)
progress(0.6, "正在合并视频和音频...")
output_file_temp = os.path.join(os.getcwd(), temp_output)
cmd_combine = [
'ffmpeg', '-y',
'-i', video_path,
'-i', audio_path,
'-c', 'copy',
output_file_temp
]
subprocess.run(cmd_combine, check=True)
progress(0.7, "正在剪辑视频片段...")
output_file = os.path.join(os.getcwd(), output_filename)
cmd_cut = [
'ffmpeg', '-y',
'-i', output_file_temp,
'-ss', str(start_seconds),
'-to', str(end_seconds),
'-c:v', 'copy',
'-c:a', 'copy',
output_file
]
subprocess.run(cmd_cut, check=True)
os.remove(video_path)
os.remove(audio_path)
os.remove(output_file_temp)
progress(1.0, "已完成!")
return output_file
else:
streams = yt.streams.filter(progressive=True).order_by('resolution').desc()
if not streams:
return "无法找到合适的视频流"
video_stream = streams.first()
print(f"Selected stream: {video_stream.resolution} {video_stream.fps}fps")
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
output_filename = f"{safe_title}_{timestamp}_{start_time.replace(':', '')}_{end_time.replace(':', '')}.mp4"
progress(0.4, "正在下载视频...")
download_path = video_stream.download(filename=f"temp_{output_filename}")
progress(0.7, "正在剪辑视频片段...")
output_file = os.path.join(os.getcwd(), output_filename)
cmd = [
'ffmpeg', '-y',
'-i', download_path,
'-ss', str(start_seconds),
'-to', str(end_seconds),
'-c:v', 'copy',
'-c:a', 'copy',
output_file
]
subprocess.run(cmd, check=True)
os.remove(download_path)
progress(1.0, "已完成!")
return output_file
except Exception as e:
return f"错误: {str(e)}"
# ====== Unified Interface Functions ======
def fetch_video_info(video_url, engine, po_token="", visitor_data="", cookies_file=None):
if engine == "yt_dlp":
return fetch_video_info_ytdlp(video_url, cookies_file)
elif engine == "pytube":
return fetch_video_info_pytube(video_url)
elif engine == "pytubefix":
return fetch_video_info_pytubefix(video_url, po_token, visitor_data)
else:
return f"错误: 未知引擎 {engine}", "", "", "", "", "", None, gr.update(visible=False), gr.update(choices=["360p", "480p", "720p", "1080p"], value="1080p")
def download_video_segment(video_url, start_time, end_time, quality, fps, engine, po_token="", visitor_data="", cookies_file=None, progress=gr.Progress()):
if engine == "yt_dlp":
return download_video_segment_ytdlp(video_url, start_time, end_time, quality, fps, progress, cookies_file)
elif engine == "pytube":
return download_video_segment_pytube(video_url, start_time, end_time, quality, fps, progress)
elif engine == "pytubefix":
return download_video_segment_pytubefix(video_url, start_time, end_time, quality, fps, progress, po_token, visitor_data)
else:
return f"错误: 未知引擎 {engine}"
# ====== Gradio UI ======
with gr.Blocks(title="YouTube 视频剪辑器", theme="NoCrypt/miku") as demo:
gr.Markdown("# YouTube 视频剪辑工具")
gr.Markdown("输入 YouTube 视频链接,查看视频信息并下载指定时间段的视频片段")
with gr.Accordion(label="下载引擎", open=False):
engine = gr.Radio(
label="选择用于下载视频的库",
choices=["yt_dlp", "pytube", "pytubefix"],
value="yt_dlp"
)
with gr.Group(visible=True) as ytdlp_group:
cookies_file = gr.File(
label="Cookies 文件 (可选 - Netscape 格式,用于访问受限视频)",
file_count="single",
type="filepath", # Change from "file" to "filepath"
file_types=[".txt"]
)
with gr.Group(visible=False) as pytubefix_group:
po_token = gr.Textbox(label="poToken (optional)", placeholder="INNERTUBE_API_KEY value")
visitor_data = gr.Textbox(label="visitorData (optional)", placeholder="CgtsZG1hLXVpLW...")
with gr.Row():
video_url = gr.Textbox(label="YouTube 视频链接", placeholder="https://www.youtube.com/watch?v=...")
fetch_button = gr.Button("获取视频信息", variant="primary")
error_message = gr.Textbox(label="状态", visible=True, interactive=False)
with gr.Group(visible=False) as video_info_group:
with gr.Row():
with gr.Column(scale=2):
thumbnail = gr.Image(label="缩略图")
with gr.Column(scale=3):
title = gr.Textbox(label="标题", interactive=False)
author = gr.Textbox(label="作者", interactive=False)
with gr.Row():
views = gr.Textbox(label="播放量", interactive=False)
duration = gr.Textbox(label="时长", interactive=False)
description = gr.Textbox(label="描述", interactive=False, lines=5)
gr.Markdown("### 选择要下载的视频片段")
with gr.Row():
start_time = gr.Textbox(label="开始时间 (HH:MM:SS)", placeholder="00:01:30")
end_time = gr.Textbox(label="结束时间 (HH:MM:SS)", placeholder="00:02:30")
with gr.Row():
quality = gr.Dropdown(label="视频质量", choices=["360p", "480p", "720p", "1080p"], value="1080p")
fps = gr.Radio(label="帧率", choices=["30fps", "60fps"], value="60fps")
download_button = gr.Button("下载视频片段", variant="primary")
output_file = gr.File(label="下载结果")
def update_engine_groups(engine_value):
return gr.update(visible=(engine_value == "pytubefix")), gr.update(visible=(engine_value == "yt_dlp"))
engine.change(
fn=update_engine_groups,
inputs=[engine],
outputs=[pytubefix_group, ytdlp_group]
)
fetch_button.click(
fn=fetch_video_info,
inputs=[video_url, engine, po_token, visitor_data, cookies_file],
outputs=[error_message, title, author, views, duration, description, thumbnail, video_info_group, quality]
)
download_button.click(
fn=download_video_segment,
inputs=[video_url, start_time, end_time, quality, fps, engine, po_token, visitor_data, cookies_file],
outputs=[output_file]
)
if __name__ == "__main__":
demo.launch()