Spaces:
Sleeping
Sleeping
import streamlit as st | |
import yt_dlp | |
import tempfile | |
import os | |
import threading | |
import time | |
from pathlib import Path | |
# 頁面配置 | |
st.set_page_config( | |
page_title="🎬 YouTube 下載器", | |
page_icon="🎬", | |
layout="centered" | |
) | |
# 初始化狀態 | |
if 'download_status' not in st.session_state: | |
st.session_state.download_status = "" | |
if 'is_downloading' not in st.session_state: | |
st.session_state.is_downloading = False | |
if 'download_progress' not in st.session_state: | |
st.session_state.download_progress = 0 | |
if 'video_info' not in st.session_state: | |
st.session_state.video_info = None | |
if 'download_path' not in st.session_state: | |
st.session_state.download_path = None | |
def progress_hook(d): | |
"""下載進度回調""" | |
if d['status'] == 'downloading': | |
if 'total_bytes' in d or '_total_bytes_str' in d: | |
try: | |
downloaded = d.get('downloaded_bytes', 0) | |
total = d.get('total_bytes') or d.get('_total_bytes_estimate', 1) | |
if total and total > 0: | |
percent = min((downloaded / total) * 100, 100) | |
st.session_state.download_progress = percent | |
st.session_state.download_status = f"📥 下載中... {percent:.1f}%" | |
else: | |
st.session_state.download_status = "📥 下載中..." | |
except: | |
st.session_state.download_status = "📥 下載中..." | |
else: | |
st.session_state.download_status = "📥 下載中..." | |
elif d['status'] == 'finished': | |
st.session_state.download_progress = 100 | |
st.session_state.download_status = "✅ 下載完成!" | |
filename = os.path.basename(d['filename']) | |
st.session_state.download_path = d['filename'] | |
def get_video_info(url): | |
"""獲取影片資訊""" | |
try: | |
ydl_opts = { | |
'quiet': True, | |
'no_warnings': True, | |
} | |
with yt_dlp.YoutubeDL(ydl_opts) as ydl: | |
info = ydl.extract_info(url, download=False) | |
return { | |
'title': info.get('title', '未知標題'), | |
'uploader': info.get('uploader', '未知上傳者'), | |
'duration': info.get('duration', 0), | |
'thumbnail': info.get('thumbnail', ''), | |
'view_count': info.get('view_count', 0) | |
} | |
except Exception as e: | |
st.error(f"❌ 無法獲取影片資訊: {str(e)}") | |
return None | |
def download_video(url, quality, audio_only): | |
"""下載影片""" | |
try: | |
# 創建臨時目錄 | |
temp_dir = tempfile.mkdtemp() | |
# 設定下載選項 | |
if audio_only: | |
format_selector = 'bestaudio/best' | |
filename = '%(title)s.%(ext)s' | |
postprocessors = [{ | |
'key': 'FFmpegExtractAudio', | |
'preferredcodec': 'mp3', | |
'preferredquality': '192', | |
}] | |
else: | |
height = quality.replace('p', '') | |
format_selector = f'best[height<={height}]/best' | |
filename = '%(title)s.%(ext)s' | |
postprocessors = [] | |
ydl_opts = { | |
'format': format_selector, | |
'outtmpl': os.path.join(temp_dir, filename), | |
'progress_hooks': [progress_hook], | |
'quiet': True, | |
'no_warnings': True, | |
'postprocessors': postprocessors, | |
} | |
st.session_state.download_status = "🚀 開始下載..." | |
with yt_dlp.YoutubeDL(ydl_opts) as ydl: | |
ydl.download([url]) | |
st.session_state.is_downloading = False | |
except Exception as e: | |
st.session_state.download_status = f"❌ 下載失敗: {str(e)}" | |
st.session_state.is_downloading = False | |
# 主介面 | |
st.title("🎬 YouTube 下載器") | |
st.markdown("簡單快速的YouTube影片下載工具") | |
# URL 輸入 | |
url = st.text_input("🔗 請輸入 YouTube 影片連結", placeholder="https://www.youtube.com/watch?v=...") | |
# 下載設定 | |
col1, col2 = st.columns(2) | |
with col1: | |
download_type = st.selectbox("📱 下載類型", ["影片", "音訊 (MP3)"]) | |
with col2: | |
if download_type == "影片": | |
quality = st.selectbox("🎯 影片品質", ["1080p", "720p", "480p", "360p"], index=1) | |
else: | |
quality = None | |
st.info("音訊品質: 192kbps MP3") | |
# 按鈕區域 | |
btn_col1, btn_col2 = st.columns(2) | |
with btn_col1: | |
if st.button("🔍 預覽影片資訊", use_container_width=True): | |
if url: | |
with st.spinner("正在獲取影片資訊..."): | |
info = get_video_info(url) | |
if info: | |
st.session_state.video_info = info | |
else: | |
st.error("請先輸入影片連結!") | |
with btn_col2: | |
if st.button("⬇️ 開始下載", use_container_width=True, disabled=st.session_state.is_downloading): | |
if not url: | |
st.error("請輸入影片連結!") | |
else: | |
st.session_state.is_downloading = True | |
st.session_state.download_progress = 0 | |
st.session_state.download_status = "準備下載..." | |
st.session_state.download_path = None | |
audio_only = download_type == "音訊 (MP3)" | |
thread = threading.Thread( | |
target=download_video, | |
args=(url, quality, audio_only), | |
daemon=True | |
) | |
thread.start() | |
# 顯示影片資訊 | |
if st.session_state.video_info: | |
st.markdown("---") | |
st.subheader("📹 影片資訊") | |
info = st.session_state.video_info | |
# 創建兩欄布局 | |
info_col1, info_col2 = st.columns([2, 1]) | |
with info_col1: | |
st.write(f"**📝 標題:** {info['title']}") | |
st.write(f"**👤 上傳者:** {info['uploader']}") | |
if info['duration']: | |
minutes = info['duration'] // 60 | |
seconds = info['duration'] % 60 | |
st.write(f"**⏱️ 長度:** {minutes}:{seconds:02d}") | |
if info['view_count']: | |
st.write(f"**👁️ 觀看次數:** {info['view_count']:,}") | |
with info_col2: | |
if info['thumbnail']: | |
st.image(info['thumbnail'], caption="影片縮圖", width=200) | |
# 顯示下載進度 | |
if st.session_state.is_downloading or st.session_state.download_progress > 0: | |
st.markdown("---") | |
st.subheader("📊 下載進度") | |
if st.session_state.download_progress > 0: | |
progress_bar = st.progress(st.session_state.download_progress / 100) | |
if st.session_state.download_status: | |
st.write(st.session_state.download_status) | |
# 下載完成後提供下載連結 | |
if st.session_state.download_path and os.path.exists(st.session_state.download_path): | |
st.markdown("---") | |
st.success("🎉 下載完成!") | |
try: | |
with open(st.session_state.download_path, 'rb') as file: | |
file_data = file.read() | |
filename = os.path.basename(st.session_state.download_path) | |
st.download_button( | |
label="📁 下載檔案", | |
data=file_data, | |
file_name=filename, | |
mime="application/octet-stream", | |
use_container_width=True | |
) | |
except Exception as e: | |
st.error(f"檔案讀取失敗: {e}") | |
# 自動刷新進度 | |
if st.session_state.is_downloading: | |
time.sleep(0.5) | |
st.rerun() | |
# 頁腳資訊 | |
st.markdown("---") | |
st.markdown(""" | |
<div style="text-align: center; color: #666;"> | |
<small>⚠️ 請確保您有權下載所選內容,並遵守相關版權法規</small> | |
</div> | |
""", unsafe_allow_html=True) |