Roberta2024 commited on
Commit
4408021
·
verified ·
1 Parent(s): 1b4b069

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +440 -38
src/streamlit_app.py CHANGED
@@ -1,40 +1,442 @@
1
- import altair as alt
2
- import numpy as np
3
- import pandas as pd
4
  import streamlit as st
 
 
 
 
 
 
 
 
5
 
6
- """
7
- # Welcome to Streamlit!
8
-
9
- Edit `/streamlit_app.py` to customize this app to your heart's desire :heart:.
10
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
11
- forums](https://discuss.streamlit.io).
12
-
13
- In the meantime, below is an example of what you can do with just a few lines of code:
14
- """
15
-
16
- num_points = st.slider("Number of points in spiral", 1, 10000, 1100)
17
- num_turns = st.slider("Number of turns in spiral", 1, 300, 31)
18
-
19
- indices = np.linspace(0, 1, num_points)
20
- theta = 2 * np.pi * num_turns * indices
21
- radius = indices
22
-
23
- x = radius * np.cos(theta)
24
- y = radius * np.sin(theta)
25
-
26
- df = pd.DataFrame({
27
- "x": x,
28
- "y": y,
29
- "idx": indices,
30
- "rand": np.random.randn(num_points),
31
- })
32
-
33
- st.altair_chart(alt.Chart(df, height=700, width=700)
34
- .mark_point(filled=True)
35
- .encode(
36
- x=alt.X("x", axis=None),
37
- y=alt.Y("y", axis=None),
38
- color=alt.Color("idx", legend=None, scale=alt.Scale()),
39
- size=alt.Size("rand", legend=None, scale=alt.Scale(range=[1, 150])),
40
- ))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import streamlit as st
2
+ import os
3
+ import threading
4
+ from pathlib import Path
5
+ import tempfile
6
+ import time
7
+ import subprocess
8
+ import sys
9
+ import random
10
 
11
+ # 检查 yt-dlp 模块
12
+ def check_yt_dlp():
13
+ try:
14
+ import yt_dlp
15
+ return True, yt_dlp
16
+ except ImportError:
17
+ return False, None
18
+
19
+ yt_dlp_installed, yt_dlp = check_yt_dlp()
20
+
21
+ if not yt_dlp_installed:
22
+ st.error("🚨 缺少必要模組 yt-dlp")
23
+ st.markdown("""
24
+ ### 請先安裝套件:
25
+ ```bash
26
+ pip install yt-dlp
27
+ ```
28
+ """)
29
+
30
+ if st.button("🔧 自動安裝 yt-dlp"):
31
+ with st.spinner("正在安裝..."):
32
+ try:
33
+ result = subprocess.run([sys.executable, "-m", "pip", "install", "yt-dlp"],
34
+ capture_output=True, text=True)
35
+ if result.returncode == 0:
36
+ st.success("✅ 安裝成功!請重新載入頁面")
37
+ st.balloons()
38
+ else:
39
+ st.error(f"❌ 安裝失敗: {result.stderr}")
40
+ except Exception as e:
41
+ st.error(f"❌ 安裝過程出錯: {e}")
42
+ st.stop()
43
+
44
+ # 页面配置
45
+ st.set_page_config(
46
+ page_title="YouTube 影片下載器",
47
+ page_icon="🎬",
48
+ layout="wide"
49
+ )
50
+
51
+ # 初始化状态
52
+ if 'video_info' not in st.session_state:
53
+ st.session_state.video_info = None
54
+ if 'download_logs' not in st.session_state:
55
+ st.session_state.download_logs = []
56
+ if 'is_downloading' not in st.session_state:
57
+ st.session_state.is_downloading = False
58
+ if 'download_progress' not in st.session_state:
59
+ st.session_state.download_progress = 0
60
+ if 'download_status' not in st.session_state:
61
+ st.session_state.download_status = "準備就緒"
62
+ if 'cookies_content' not in st.session_state:
63
+ st.session_state.cookies_content = ""
64
+
65
+ def add_log(message):
66
+ """添加日志"""
67
+ timestamp = time.strftime("%H:%M:%S")
68
+ st.session_state.download_logs.append(f"[{timestamp}] {message}")
69
+ if len(st.session_state.download_logs) > 50:
70
+ st.session_state.download_logs.pop(0)
71
+
72
+ def get_default_download_path():
73
+ """获取默认下载路径"""
74
+ try:
75
+ downloads_path = Path.home() / "Downloads"
76
+ if downloads_path.exists():
77
+ return str(downloads_path)
78
+ return str(Path.home())
79
+ except:
80
+ return tempfile.gettempdir()
81
+
82
+ def get_random_user_agent():
83
+ """获取随机User-Agent"""
84
+ user_agents = [
85
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
86
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1',
87
+ 'Mozilla/5.0 (Linux; Android 13; SM-G991B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36',
88
+ ]
89
+ return random.choice(user_agents)
90
+
91
+ def create_cookies_file(cookies_content):
92
+ """创建cookies文件"""
93
+ if not cookies_content.strip():
94
+ return None
95
+ try:
96
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f:
97
+ if not cookies_content.startswith('# Netscape HTTP Cookie File'):
98
+ f.write('# Netscape HTTP Cookie File\n')
99
+ f.write(cookies_content)
100
+ return f.name
101
+ except Exception as e:
102
+ add_log(f"❌ 無法創建 cookies 檔案: {e}")
103
+ return None
104
+
105
+ def get_ydl_options(cookies_file=None, use_mobile=False):
106
+ """获取yt-dlp选项"""
107
+ user_agent = get_random_user_agent()
108
+
109
+ options = {
110
+ 'quiet': False,
111
+ 'verbose': True,
112
+ 'http_headers': {
113
+ 'User-Agent': user_agent,
114
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
115
+ 'Accept-Language': 'en-US,en;q=0.9,zh-TW;q=0.8',
116
+ },
117
+ 'geo_bypass': True,
118
+ 'extractor_args': {
119
+ 'youtube': {
120
+ 'skip': ['hls'],
121
+ 'player_client': ['android', 'web'] if use_mobile else ['web', 'android'],
122
+ }
123
+ },
124
+ 'retries': 3,
125
+ 'socket_timeout': 30,
126
+ 'no_check_certificate': True,
127
+ }
128
+
129
+ if cookies_file and os.path.exists(cookies_file):
130
+ options['cookiefile'] = cookies_file
131
+ add_log("📋 使用 cookies 檔案")
132
+
133
+ return options
134
+
135
+ def get_video_info(url, use_cookies=False, use_mobile=False):
136
+ """获取视频信息"""
137
+ if not url.strip():
138
+ st.error("請輸入影片連結!")
139
+ return None
140
+
141
+ add_log("🔍 正在獲取影片資訊...")
142
+
143
+ methods = [
144
+ {'name': '標準模式', 'mobile': False},
145
+ {'name': '行動版API', 'mobile': True},
146
+ ]
147
+
148
+ for method in methods:
149
+ try:
150
+ add_log(f"🔄 嘗試 {method['name']}...")
151
+
152
+ cookies_file = None
153
+ if use_cookies and st.session_state.cookies_content:
154
+ cookies_file = create_cookies_file(st.session_state.cookies_content)
155
+
156
+ ydl_opts = get_ydl_options(cookies_file=cookies_file, use_mobile=method['mobile'])
157
+
158
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
159
+ info = ydl.extract_info(url, download=False)
160
+
161
+ if info:
162
+ video_info = {
163
+ 'title': info.get('title', '未知標題'),
164
+ 'uploader': info.get('uploader', '未知上傳者'),
165
+ 'duration': info.get('duration', 0),
166
+ 'view_count': info.get('view_count', 0),
167
+ 'thumbnail': info.get('thumbnail', ''),
168
+ 'description': info.get('description', '')[:200] + '...' if info.get('description') else '',
169
+ 'method_used': method['name']
170
+ }
171
+
172
+ add_log(f"✅ {method['name']} 成功獲取影片資訊")
173
+ return video_info
174
+
175
+ if cookies_file and os.path.exists(cookies_file):
176
+ os.unlink(cookies_file)
177
+
178
+ except Exception as e:
179
+ add_log(f"❌ {method['name']} 失敗: {str(e)[:100]}...")
180
+ if 'cookies_file' in locals() and cookies_file and os.path.exists(cookies_file):
181
+ os.unlink(cookies_file)
182
+ continue
183
+
184
+ add_log("❌ 所有方法都失敗了")
185
+ st.error("❌ 無法獲取影片資訊,請嘗試使用 Cookies 或稍後重試")
186
+ return None
187
+
188
+ def progress_hook(d):
189
+ """下载进度回调"""
190
+ if d['status'] == 'downloading':
191
+ if 'total_bytes' in d:
192
+ downloaded = d.get('downloaded_bytes', 0)
193
+ total = d.get('total_bytes', 1)
194
+ percent = (downloaded / total) * 100
195
+ st.session_state.download_progress = percent
196
+ st.session_state.download_status = f"下載中... {percent:.1f}%"
197
+ else:
198
+ st.session_state.download_status = "下載中..."
199
+ elif d['status'] == 'finished':
200
+ st.session_state.download_progress = 100
201
+ st.session_state.download_status = "下載完成!"
202
+ filename = os.path.basename(d['filename'])
203
+ add_log(f"✅ 下載完成: {filename}")
204
+
205
+ def download_video(url, quality, audio_only, download_path, use_cookies=False, use_mobile=False):
206
+ """下载视频"""
207
+ try:
208
+ os.makedirs(download_path, exist_ok=True)
209
+
210
+ add_log("🚀 開始下載...")
211
+ add_log(f"🎯 類型: {'音訊 (MP3)' if audio_only else f'影片 ({quality})'}")
212
+
213
+ methods = [
214
+ {'name': '標準下載', 'mobile': False},
215
+ {'name': '行動版下載', 'mobile': True},
216
+ ]
217
+
218
+ for method in methods:
219
+ try:
220
+ add_log(f"🔄 嘗試 {method['name']}...")
221
+
222
+ cookies_file = None
223
+ if use_cookies and st.session_state.cookies_content:
224
+ cookies_file = create_cookies_file(st.session_state.cookies_content)
225
+
226
+ # 设置格式
227
+ if audio_only:
228
+ format_selector = 'bestaudio/best'
229
+ filename = '%(title)s.%(ext)s'
230
+ else:
231
+ height = quality.replace('p', '')
232
+ format_selector = f'best[height<={height}]/best'
233
+ filename = '%(title)s.%(ext)s'
234
+
235
+ ydl_opts = get_ydl_options(cookies_file=cookies_file, use_mobile=method['mobile'])
236
+ ydl_opts.update({
237
+ 'format': format_selector,
238
+ 'outtmpl': os.path.join(download_path, filename),
239
+ 'progress_hooks': [progress_hook],
240
+ 'noplaylist': True,
241
+ })
242
+
243
+ # 音频转换
244
+ if audio_only:
245
+ ydl_opts['postprocessors'] = [{
246
+ 'key': 'FFmpegExtractAudio',
247
+ 'preferredcodec': 'mp3',
248
+ 'preferredquality': '192',
249
+ }]
250
+
251
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
252
+ ydl.download([url])
253
+
254
+ add_log(f"🎉 {method['name']} 下載成功完成!")
255
+ st.session_state.is_downloading = False
256
+ st.success("下載完成!")
257
+ return
258
+
259
+ except Exception as e:
260
+ add_log(f"❌ {method['name']} 失敗: {str(e)[:100]}...")
261
+ if cookies_file and os.path.exists(cookies_file):
262
+ os.unlink(cookies_file)
263
+ continue
264
+
265
+ # 所有方法失败
266
+ st.session_state.download_status = "下載失敗"
267
+ add_log("❌ 所有下載方法都失敗")
268
+ st.session_state.is_downloading = False
269
+ st.error("下載失敗,請檢查網路連線或嘗試使用 Cookies")
270
+
271
+ except Exception as e:
272
+ st.session_state.download_status = "下載失敗"
273
+ error_msg = f"下載過程出錯: {str(e)}"
274
+ add_log(f"❌ {error_msg}")
275
+ st.session_state.is_downloading = False
276
+ st.error(error_msg)
277
+
278
+ # 主界面
279
+ st.title("🎬 YouTube 影片下載器")
280
+ st.markdown("---")
281
+
282
+ # 侧边栏设置
283
+ with st.sidebar:
284
+ st.header("⚙️ 下載設定")
285
+
286
+ # 下载类型
287
+ download_type = st.radio("下載類型", ["影片", "音訊 (MP3)"])
288
+
289
+ # 质量选择
290
+ if download_type == "影片":
291
+ quality = st.selectbox("影片品質", ["1080p", "720p", "480p", "360p"], index=1)
292
+ else:
293
+ quality = None
294
+ st.info("音訊將以 192kbps MP3 格式下載")
295
+
296
+ # 下载路径
297
+ st.subheader("📁 下載路徑")
298
+ default_path = get_default_download_path()
299
+ download_path = st.text_input("路徑", value=default_path)
300
+
301
+ if not os.path.exists(download_path):
302
+ st.warning("⚠️ 路徑不存在")
303
+ else:
304
+ st.success("✅ 路徑有效")
305
+
306
+ st.markdown("---")
307
+
308
+ # 高级设置
309
+ st.subheader("🔧 進階設定")
310
+ use_cookies = st.checkbox("使用 Cookies(推薦)", value=True)
311
+
312
+ if use_cookies:
313
+ st.info("輸入 YouTube cookies 可解決認證問題")
314
+ cookies_input = st.text_area(
315
+ "貼上 cookies(Netscape 格式)",
316
+ placeholder="# Netscape HTTP Cookie File\n.youtube.com\tTRUE\t/\tFALSE\t...",
317
+ height=100
318
+ )
319
+ if cookies_input != st.session_state.cookies_content:
320
+ st.session_state.cookies_content = cookies_input
321
+
322
+ use_mobile = st.checkbox("使用行動版 API")
323
+
324
+ # 主内容
325
+ col1, col2 = st.columns([2, 1])
326
+
327
+ with col1:
328
+ # URL输入
329
+ st.subheader("🔗 影片連結")
330
+ url = st.text_input("請輸入 YouTube 影片連結", placeholder="https://www.youtube.com/watch?v=...")
331
+
332
+ col_btn1, col_btn2, col_btn3 = st.columns(3)
333
+
334
+ with col_btn1:
335
+ if st.button("🔍 獲取影片資訊", type="primary"):
336
+ if url:
337
+ with st.spinner("正在獲取影片資訊..."):
338
+ result = get_video_info(url, use_cookies, use_mobile)
339
+ if result:
340
+ st.session_state.video_info = result
341
+ else:
342
+ st.error("請先輸入影片連結!")
343
+
344
+ with col_btn2:
345
+ if st.button("🚀 開始下載", disabled=st.session_state.is_downloading):
346
+ if not url:
347
+ st.error("請輸入影片連結!")
348
+ else:
349
+ st.session_state.is_downloading = True
350
+ st.session_state.download_progress = 0
351
+ st.session_state.download_status = "準備下載..."
352
+
353
+ audio_only = download_type == "音訊 (MP3)"
354
+ thread = threading.Thread(
355
+ target=download_video,
356
+ args=(url, quality, audio_only, download_path, use_cookies, use_mobile),
357
+ daemon=True
358
+ )
359
+ thread.start()
360
+
361
+ with col_btn3:
362
+ if st.button("🔄 更新 yt-dlp"):
363
+ with st.spinner("正在更新..."):
364
+ try:
365
+ result = subprocess.run([sys.executable, "-m", "pip", "install", "--upgrade", "yt-dlp"],
366
+ capture_output=True, text=True)
367
+ if result.returncode == 0:
368
+ st.success("✅ 更新成功!")
369
+ add_log("✅ yt-dlp 已更新到最新版本")
370
+ else:
371
+ st.error(f"❌ 更新失敗")
372
+ except Exception as e:
373
+ st.error(f"❌ 更新失敗: {e}")
374
+
375
+ # 视频信息显示
376
+ if st.session_state.video_info:
377
+ st.subheader("📹 影片資訊")
378
+ info = st.session_state.video_info
379
+
380
+ info_col1, info_col2 = st.columns([3, 1])
381
+
382
+ with info_col1:
383
+ st.write(f"**標題:** {info['title']}")
384
+ st.write(f"**上傳者:** {info['uploader']}")
385
+ if info['duration']:
386
+ st.write(f"**長度:** {info['duration']//60}:{info['duration']%60:02d}")
387
+ if info['view_count']:
388
+ st.write(f"**觀看次數:** {info['view_count']:,}")
389
+ st.success(f"**使用方法:** {info['method_used']}")
390
+
391
+ if info['description']:
392
+ with st.expander("📝 影片描述"):
393
+ st.write(info['description'])
394
+
395
+ with info_col2:
396
+ if info['thumbnail']:
397
+ st.image(info['thumbnail'], caption="影片縮圖")
398
+
399
+ # 下载进度
400
+ if st.session_state.is_downloading or st.session_state.download_progress > 0:
401
+ st.subheader("📊 下載進度")
402
+ progress_bar = st.progress(st.session_state.download_progress / 100)
403
+ st.write(f"狀態: {st.session_state.download_status}")
404
+
405
+ with col2:
406
+ # 下载日志
407
+ st.subheader("📋 下載日誌")
408
+
409
+ if st.button("🗑️ 清除日誌"):
410
+ st.session_state.download_logs = []
411
+ st.rerun()
412
+
413
+ if st.session_state.download_logs:
414
+ log_text = "\n".join(st.session_state.download_logs[-15:])
415
+ st.text_area("", value=log_text, height=250, disabled=True)
416
+ else:
417
+ st.info("暫無日誌")
418
+
419
+ # 自动刷新
420
+ if st.session_state.is_downloading:
421
+ time.sleep(1)
422
+ st.rerun()
423
+
424
+ # 故障排除指南
425
+ st.markdown("---")
426
+ with st.expander("🔧 故障排除指南"):
427
+ st.markdown("""
428
+ ### 針對認證錯誤的解決方案:
429
+
430
+ **推薦方法:使用 Cookies**
431
+ 1. 在瀏覽器中登入 YouTube
432
+ 2. 安裝 "Get cookies.txt" 擴展
433
+ 3. 在 YouTube 頁面點擊擴展
434
+ 4. 複製 cookies 內容到左側設定區
435
+ 5. 勾選"使用 Cookies"並重試下載
436
+
437
+ **其他方法:**
438
+ - 嘗試行動版 API
439
+ - 等待幾分鐘後重試
440
+ - 檢查網路連線
441
+ - 更新 yt-dlp 到最新版本
442
+ """)