Streamlit Cloud上基于yt-dlp的视频下载应用无法将服务器文件推送到用户本地的问题求助
Streamlit Cloud上基于yt-dlp的视频下载应用无法将服务器文件推送到用户本地的问题求助
嗨,我理解你遇到的问题——本地运行一切正常,但部署到Streamlit Cloud后,视频只存在服务器上,用户点了下载按钮却拿不到本地文件。我之前也碰到过类似的Streamlit文件下载问题,帮你分析下原因和解决方案:
核心问题分析
你的代码逻辑是先让服务器下载视频到临时目录,再通过st.download_button把文件传给用户,但这里有几个潜在的坑:
- 双重按钮的认知偏差:你现在有两个按钮——第一个触发服务器下载,第二个才是真正的客户端下载。很多用户会以为第一个按钮直接完成下载,忽略了需要点击第二个生成的按钮。
- Streamlit会话与临时文件的生命周期冲突:Streamlit Cloud的临时目录(
tempfile.mkdtemp()创建的)会在会话结束或超时后被自动清理,可能用户还没来得及点击第二个按钮,文件就已经被删掉了。 - Playlist场景的逻辑漏洞:当前代码只取临时目录里的第一个文件,如果用户下载的是播放列表,会丢失其他视频文件。
针对性解决方案
我调整了你的代码,解决了这些问题:
- 把服务器下载和客户端下载的逻辑整合,避免双重按钮混淆用户
- 直接把视频文件读取到内存中,不依赖服务器临时文件,确保会话内文件始终可用
- 优化了Playlist的处理逻辑(如果需要下载整个播放列表,可以打包成ZIP)
修改后的代码如下:
import streamlit as st import subprocess import os import tempfile from zipfile import ZipFile import io # 设置页面配置 st.set_page_config(page_title="YouTube Downloader", layout="centered") st.title("YouTube Video Downloader") # 初始化会话状态,保存文件内容和文件名 if "file_data" not in st.session_state: st.session_state.file_data = None if "file_name" not in st.session_state: st.session_state.file_name = None # 函数:下载视频并返回文件内容(内存中)、文件名和MIME类型 def download_video(url, is_playlist, quality, subtitles): temp_dir = tempfile.mkdtemp() # 构建yt-dlp命令 cmd = ["yt-dlp", "-o", os.path.join(temp_dir, "%(title)s.%(ext)s")] # 质量参数映射,简化冗余代码 quality_map = { "144p": "bv*[height<=144][ext=mp4]+ba[ext=m4a]/bv*[ext=mp4]+ba[ext=m4a]/b[ext=mp4]", "240p": "bv*[height<=240][ext=mp4]+ba[ext=m4a]/bv*[ext=mp4]+ba[ext=m4a]/b[ext=mp4]", "360p": "bv*[height<=360][ext=mp4]+ba[ext=m4a]/bv*[ext=mp4]+ba[ext=m4a]/b[ext=mp4]", "480p": "bv*[height<=480][ext=mp4]+ba[ext=m4a]/bv*[ext=mp4]+ba[ext=m4a]/b[ext=mp4]", "720p": "bv*[height<=720][ext=mp4]+ba[ext=m4a]/bv*[ext=mp4]+ba[ext=m4a]/b[ext=mp4]", "1080p": "bv*[height<=1080][ext=mp4]+ba[ext=m4a]/bv*[ext=mp4]+ba[ext=m4a]/b[ext=mp4]", "1440p": "bv*[height<=1440][ext=mp4]+ba[ext=m4a]/bv*[ext=mp4]+ba[ext=m4a]/b[ext=mp4]", "2160p": "bv*[height<=2160][ext=mp4]+ba[ext=m4a]/bv*[ext=mp4]+ba[ext=m4a]/b[ext=mp4]", "Best Available": "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]" } cmd += ["-f", quality_map.get(quality, quality_map["Best Available"])] # 输出格式和字幕配置 cmd += ["--merge-output-format", "mp4"] if subtitles: cmd += ["--write-sub", "--sub-lang", "en", "--embed-subs"] # 播放列表文件名配置 if is_playlist: cmd += ["-o", os.path.join(temp_dir, "%(playlist_index)s. %(title)s.%(ext)s")] cmd.append(url) # 执行下载命令 subprocess.run(cmd, check=True) # 处理文件:播放列表打包ZIP,单文件直接读取 video_files = [f for f in os.listdir(temp_dir) if f.endswith(('.mp4', '.mkv', '.webm'))] if is_playlist and len(video_files) > 1: zip_buffer = io.BytesIO() with ZipFile(zip_buffer, "w") as zip_file: for file in video_files: zip_file.write(os.path.join(temp_dir, file), file) zip_buffer.seek(0) return zip_buffer, "youtube_playlist.zip", "application/zip" else: file_path = os.path.join(temp_dir, video_files[0]) with open(file_path, "rb") as f: file_data = f.read() return file_data, os.path.basename(file_path), "video/mp4" # UI输入区域 with st.container(): st.markdown("### Input Video Details") url = st.text_input("Enter YouTube URL", key="url_input") option = st.selectbox("Download Type", ("Single Video", "Playlist"), key="download_type") is_playlist = option == "Playlist" with st.container(): st.markdown("### Options") quality = st.selectbox( "Select Video Quality", ["144p", "240p", "360p", "480p", "720p", "1080p", "1440p", "2160p", "Best Available"], index=4, key="quality_select" ) subtitles = st.checkbox("Add Subtitles", key="subtitles_checkbox") # 触发下载的主按钮 if st.button("Start Download & Prepare File", key="download_button"): if not url: st.error("Please provide a valid YouTube URL.") else: try: with st.spinner("Downloading and preparing your file..."): # 把文件内容存入会话状态 st.session_state.file_data, st.session_state.file_name, mime_type = download_video(url, is_playlist, quality, subtitles) st.success("File is ready for download!") except subprocess.CalledProcessError: st.error("An error occurred during the download. Please check the URL and try again.") # 当文件准备好时,显示最终下载按钮 if st.session_state.file_data is not None: st.download_button( label=f"Download {st.session_state.file_name}", data=st.session_state.file_data, file_name=st.session_state.file_name, mime=mime_type, key="final_download_button" )
关键调整说明
- 会话状态保存文件内容:用
st.session_state把文件字节流存在内存里,避免临时文件被Streamlit Cloud自动清理,确保用户随时能点击下载。 - 清晰的操作流程:第一个按钮负责完成服务器端的下载和文件准备,完成后明确提示用户点击专门的下载按钮,减少认知混淆。
- 完整的Playlist支持:自动检测播放列表,将多个视频打包成ZIP文件,方便用户一次性下载所有内容。
- 内存读取替代文件路径依赖:直接把文件读入内存,不再依赖服务器本地文件路径,彻底避免路径失效的问题。
你可以把这段代码部署到Streamlit Cloud试试,应该就能解决文件无法推送到用户本地的问题了。
备注:内容来源于stack exchange,提问作者Asad Sheikh




