Fix YouTube example URL processing in Spaces environment
Browse files- app.py +16 -2
- youtube_api.py +52 -34
app.py
CHANGED
@@ -168,8 +168,11 @@ def process_youtube_url(youtube_url, user_api_key=None):
|
|
168 |
# 處理 YouTube URL
|
169 |
print(f"Processing YouTube URL: {youtube_url}")
|
170 |
|
171 |
-
|
172 |
-
|
|
|
|
|
|
|
173 |
print("Warning: YouTube download is not supported in Hugging Face Spaces without an API key.")
|
174 |
raise gr.Error("YouTube 下載在 Hugging Face Spaces 中需要 API 金鑰。請在上方的 'YouTube API Key Settings' 中輸入您的 API 金鑰。\n\nYouTube download in Hugging Face Spaces requires an API key. Please enter your API key in the 'YouTube API Key Settings' section above.")
|
175 |
|
@@ -865,6 +868,17 @@ with gr.Blocks(css=compact_css, theme=gr.themes.Default(spacing_size=gr.themes.s
|
|
865 |
# 添加範例,點擊時自動處理
|
866 |
def process_example_url(url):
|
867 |
"""處理範例 URL 的函數"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
868 |
# 獲取 API 金鑰
|
869 |
api_key = youtube_api_key_input.value if hasattr(youtube_api_key_input, 'value') else None
|
870 |
# 處理 URL
|
|
|
168 |
# 處理 YouTube URL
|
169 |
print(f"Processing YouTube URL: {youtube_url}")
|
170 |
|
171 |
+
# 檢查是否是在啟動時自動調用(緩存範例)
|
172 |
+
is_startup = not hasattr(youtube_api_key_input, 'value') if 'youtube_api_key_input' in globals() else False
|
173 |
+
|
174 |
+
if is_spaces and not youtube_api_key and not is_startup:
|
175 |
+
# 在 Spaces 環境中且沒有 API 金鑰,顯示警告(但不是在啟動時)
|
176 |
print("Warning: YouTube download is not supported in Hugging Face Spaces without an API key.")
|
177 |
raise gr.Error("YouTube 下載在 Hugging Face Spaces 中需要 API 金鑰。請在上方的 'YouTube API Key Settings' 中輸入您的 API 金鑰。\n\nYouTube download in Hugging Face Spaces requires an API key. Please enter your API key in the 'YouTube API Key Settings' section above.")
|
178 |
|
|
|
868 |
# 添加範例,點擊時自動處理
|
869 |
def process_example_url(url):
|
870 |
"""處理範例 URL 的函數"""
|
871 |
+
# 檢查是否在 Hugging Face Spaces 環境中
|
872 |
+
is_spaces = os.environ.get("SPACE_ID") is not None
|
873 |
+
|
874 |
+
# 檢查是否是在啟動時自動調用(緩存範例)
|
875 |
+
is_startup = not hasattr(youtube_api_key_input, 'value')
|
876 |
+
|
877 |
+
# 如果是在 Spaces 環境中啟動時調用,則不處理 URL
|
878 |
+
if is_spaces and is_startup:
|
879 |
+
print("Skipping example URL processing during startup in Spaces environment")
|
880 |
+
return gr.update(visible=False, value=None), gr.update(visible=False, value=None)
|
881 |
+
|
882 |
# 獲取 API 金鑰
|
883 |
api_key = youtube_api_key_input.value if hasattr(youtube_api_key_input, 'value') else None
|
884 |
# 處理 URL
|
youtube_api.py
CHANGED
@@ -39,28 +39,28 @@ def get_video_info(video_id):
|
|
39 |
"""使用 YouTube Data API 獲取視頻信息"""
|
40 |
if not YOUTUBE_API_KEY:
|
41 |
raise ValueError("YouTube API 金鑰未設置。請先調用 set_api_key() 函數。")
|
42 |
-
|
43 |
try:
|
44 |
youtube = build(YOUTUBE_API_SERVICE_NAME, YOUTUBE_API_VERSION, developerKey=YOUTUBE_API_KEY)
|
45 |
-
|
46 |
# 獲取視頻詳細信息
|
47 |
video_response = youtube.videos().list(
|
48 |
part="snippet,contentDetails,statistics",
|
49 |
id=video_id
|
50 |
).execute()
|
51 |
-
|
52 |
# 檢查是否找到視頻
|
53 |
if not video_response.get("items"):
|
54 |
return None
|
55 |
-
|
56 |
video_info = video_response["items"][0]
|
57 |
snippet = video_info["snippet"]
|
58 |
content_details = video_info["contentDetails"]
|
59 |
-
|
60 |
# 解析時長
|
61 |
duration_str = content_details["duration"] # 格式: PT#H#M#S
|
62 |
duration_seconds = parse_duration(duration_str)
|
63 |
-
|
64 |
# 返回視頻信息
|
65 |
return {
|
66 |
"title": snippet["title"],
|
@@ -70,7 +70,7 @@ def get_video_info(video_id):
|
|
70 |
"duration": duration_seconds,
|
71 |
"thumbnail": snippet["thumbnails"]["high"]["url"] if "high" in snippet["thumbnails"] else snippet["thumbnails"]["default"]["url"]
|
72 |
}
|
73 |
-
|
74 |
except HttpError as e:
|
75 |
print(f"YouTube API 錯誤: {e}")
|
76 |
return None
|
@@ -82,50 +82,50 @@ def parse_duration(duration_str):
|
|
82 |
"""解析 ISO 8601 時長格式 (PT#H#M#S)"""
|
83 |
duration_str = duration_str[2:] # 移除 "PT"
|
84 |
hours, minutes, seconds = 0, 0, 0
|
85 |
-
|
86 |
# 解析小時
|
87 |
if "H" in duration_str:
|
88 |
hours_part = duration_str.split("H")[0]
|
89 |
hours = int(hours_part)
|
90 |
duration_str = duration_str.split("H")[1]
|
91 |
-
|
92 |
# 解析分鐘
|
93 |
if "M" in duration_str:
|
94 |
minutes_part = duration_str.split("M")[0]
|
95 |
minutes = int(minutes_part)
|
96 |
duration_str = duration_str.split("M")[1]
|
97 |
-
|
98 |
# 解析秒
|
99 |
if "S" in duration_str:
|
100 |
seconds_part = duration_str.split("S")[0]
|
101 |
seconds = int(seconds_part)
|
102 |
-
|
103 |
# 計算總秒數
|
104 |
total_seconds = hours * 3600 + minutes * 60 + seconds
|
105 |
return total_seconds
|
106 |
|
107 |
def download_audio(video_id, api_info=None):
|
108 |
"""下載 YouTube 視頻的音頻
|
109 |
-
|
110 |
Args:
|
111 |
video_id: YouTube 視頻 ID
|
112 |
api_info: 從 API 獲取的視頻信息 (可選)
|
113 |
-
|
114 |
Returns:
|
115 |
tuple: (音頻文件路徑, 臨時目錄, 視頻時長)
|
116 |
"""
|
117 |
# 使用固定的目錄來存儲下載的音訊文件
|
118 |
download_dir = os.path.join(tempfile.gettempdir(), "youtube_downloads")
|
119 |
os.makedirs(download_dir, exist_ok=True)
|
120 |
-
|
121 |
# 使用視頻 ID 和時間戳作為文件名
|
122 |
filename = f"youtube_{video_id}_{int(time.time())}"
|
123 |
temp_dir = tempfile.mkdtemp()
|
124 |
-
|
125 |
try:
|
126 |
# 準備下載路徑
|
127 |
temp_filepath_tmpl = os.path.join(download_dir, f"{filename}.%(ext)s")
|
128 |
-
|
129 |
# 設置 yt-dlp 選項
|
130 |
ydl_opts = {
|
131 |
'format': 'bestaudio/best',
|
@@ -139,15 +139,15 @@ def download_audio(video_id, api_info=None):
|
|
139 |
}],
|
140 |
'ffmpeg_location': shutil.which("ffmpeg"),
|
141 |
}
|
142 |
-
|
143 |
# 檢查 ffmpeg
|
144 |
if not ydl_opts['ffmpeg_location']:
|
145 |
print("Warning: ffmpeg not found... / 警告:找不到 ffmpeg...")
|
146 |
-
|
147 |
# 如果已經有 API 信息,使用它
|
148 |
duration = api_info["duration"] if api_info else None
|
149 |
title = api_info["title"] if api_info else "Unknown"
|
150 |
-
|
151 |
# 下載音頻
|
152 |
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
153 |
# 如果沒有 API 信息,從 yt-dlp 獲取
|
@@ -158,10 +158,10 @@ def download_audio(video_id, api_info=None):
|
|
158 |
else:
|
159 |
# 有 API 信息,直接下載
|
160 |
ydl.download([f"https://www.youtube.com/watch?v={video_id}"])
|
161 |
-
|
162 |
# 確定最終文件路徑
|
163 |
final_filepath = os.path.join(download_dir, f"{filename}.mp3")
|
164 |
-
|
165 |
# 檢查文件是否存在
|
166 |
if os.path.exists(final_filepath):
|
167 |
print(f"YouTube audio downloaded: {final_filepath}")
|
@@ -170,8 +170,8 @@ def download_audio(video_id, api_info=None):
|
|
170 |
else:
|
171 |
# 嘗試查找可能的文件
|
172 |
potential_files = [
|
173 |
-
os.path.join(download_dir, f)
|
174 |
-
for f in os.listdir(download_dir)
|
175 |
if f.startswith(filename) and f.endswith(".mp3")
|
176 |
]
|
177 |
if potential_files:
|
@@ -180,7 +180,7 @@ def download_audio(video_id, api_info=None):
|
|
180 |
return downloaded_path, temp_dir, duration
|
181 |
else:
|
182 |
raise FileNotFoundError(f"Audio file not found after download in {download_dir}")
|
183 |
-
|
184 |
except Exception as e:
|
185 |
print(f"Error downloading YouTube audio: {e}")
|
186 |
if temp_dir and os.path.exists(temp_dir):
|
@@ -190,40 +190,58 @@ def download_audio(video_id, api_info=None):
|
|
190 |
print(f"Error cleaning temp directory {temp_dir}: {cleanup_e}")
|
191 |
return None, None, None
|
192 |
|
193 |
-
def process_youtube_url(youtube_url):
|
194 |
"""處理 YouTube URL,獲取信息並下載音頻
|
195 |
-
|
196 |
Args:
|
197 |
youtube_url: YouTube 視頻 URL
|
198 |
-
|
|
|
199 |
Returns:
|
200 |
tuple: (音頻文件路徑, 視頻信息)
|
201 |
"""
|
202 |
# 檢查 URL 是否有效
|
203 |
if not youtube_url or not youtube_url.strip():
|
204 |
return None, None
|
205 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
206 |
# 提取視頻 ID
|
207 |
video_id = extract_video_id(youtube_url)
|
208 |
if not video_id:
|
209 |
print(f"Invalid YouTube URL: {youtube_url}")
|
210 |
return None, None
|
211 |
-
|
212 |
# 檢查是否設置了 API 金鑰
|
213 |
if YOUTUBE_API_KEY:
|
214 |
# 使用 API 獲取視頻信息
|
215 |
video_info = get_video_info(video_id)
|
216 |
if not video_info:
|
217 |
print(f"Could not get video info from API for: {video_id}")
|
|
|
|
|
|
|
|
|
|
|
218 |
# 如果 API 失敗,嘗試直接下載
|
219 |
audio_path, temp_dir, duration = download_audio(video_id)
|
220 |
return audio_path, {"title": "Unknown", "duration": duration}
|
221 |
-
|
222 |
# 使用 API 信息下載音頻
|
223 |
audio_path, temp_dir, _ = download_audio(video_id, video_info)
|
224 |
return audio_path, video_info
|
225 |
else:
|
226 |
-
# 沒有 API
|
|
|
|
|
|
|
|
|
|
|
227 |
print("No YouTube API key set, using yt-dlp directly")
|
228 |
audio_path, temp_dir, duration = download_audio(video_id)
|
229 |
return audio_path, {"title": "Unknown", "duration": duration}
|
@@ -233,13 +251,13 @@ if __name__ == "__main__":
|
|
233 |
# 設置 API 金鑰(實際使用時應從環境變量或配置文件獲取)
|
234 |
api_key = "YOUR_API_KEY"
|
235 |
set_api_key(api_key)
|
236 |
-
|
237 |
# 測試 URL
|
238 |
test_url = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
|
239 |
-
|
240 |
# 處理 URL
|
241 |
audio_path, video_info = process_youtube_url(test_url)
|
242 |
-
|
243 |
if audio_path and video_info:
|
244 |
print(f"Downloaded: {audio_path}")
|
245 |
print(f"Video info: {video_info}")
|
|
|
39 |
"""使用 YouTube Data API 獲取視頻信息"""
|
40 |
if not YOUTUBE_API_KEY:
|
41 |
raise ValueError("YouTube API 金鑰未設置。請先調用 set_api_key() 函數。")
|
42 |
+
|
43 |
try:
|
44 |
youtube = build(YOUTUBE_API_SERVICE_NAME, YOUTUBE_API_VERSION, developerKey=YOUTUBE_API_KEY)
|
45 |
+
|
46 |
# 獲取視頻詳細信息
|
47 |
video_response = youtube.videos().list(
|
48 |
part="snippet,contentDetails,statistics",
|
49 |
id=video_id
|
50 |
).execute()
|
51 |
+
|
52 |
# 檢查是否找到視頻
|
53 |
if not video_response.get("items"):
|
54 |
return None
|
55 |
+
|
56 |
video_info = video_response["items"][0]
|
57 |
snippet = video_info["snippet"]
|
58 |
content_details = video_info["contentDetails"]
|
59 |
+
|
60 |
# 解析時長
|
61 |
duration_str = content_details["duration"] # 格式: PT#H#M#S
|
62 |
duration_seconds = parse_duration(duration_str)
|
63 |
+
|
64 |
# 返回視頻信息
|
65 |
return {
|
66 |
"title": snippet["title"],
|
|
|
70 |
"duration": duration_seconds,
|
71 |
"thumbnail": snippet["thumbnails"]["high"]["url"] if "high" in snippet["thumbnails"] else snippet["thumbnails"]["default"]["url"]
|
72 |
}
|
73 |
+
|
74 |
except HttpError as e:
|
75 |
print(f"YouTube API 錯誤: {e}")
|
76 |
return None
|
|
|
82 |
"""解析 ISO 8601 時長格式 (PT#H#M#S)"""
|
83 |
duration_str = duration_str[2:] # 移除 "PT"
|
84 |
hours, minutes, seconds = 0, 0, 0
|
85 |
+
|
86 |
# 解析小時
|
87 |
if "H" in duration_str:
|
88 |
hours_part = duration_str.split("H")[0]
|
89 |
hours = int(hours_part)
|
90 |
duration_str = duration_str.split("H")[1]
|
91 |
+
|
92 |
# 解析分鐘
|
93 |
if "M" in duration_str:
|
94 |
minutes_part = duration_str.split("M")[0]
|
95 |
minutes = int(minutes_part)
|
96 |
duration_str = duration_str.split("M")[1]
|
97 |
+
|
98 |
# 解析秒
|
99 |
if "S" in duration_str:
|
100 |
seconds_part = duration_str.split("S")[0]
|
101 |
seconds = int(seconds_part)
|
102 |
+
|
103 |
# 計算總秒數
|
104 |
total_seconds = hours * 3600 + minutes * 60 + seconds
|
105 |
return total_seconds
|
106 |
|
107 |
def download_audio(video_id, api_info=None):
|
108 |
"""下載 YouTube 視頻的音頻
|
109 |
+
|
110 |
Args:
|
111 |
video_id: YouTube 視頻 ID
|
112 |
api_info: 從 API 獲取的視頻信息 (可選)
|
113 |
+
|
114 |
Returns:
|
115 |
tuple: (音頻文件路徑, 臨時目錄, 視頻時長)
|
116 |
"""
|
117 |
# 使用固定的目錄來存儲下載的音訊文件
|
118 |
download_dir = os.path.join(tempfile.gettempdir(), "youtube_downloads")
|
119 |
os.makedirs(download_dir, exist_ok=True)
|
120 |
+
|
121 |
# 使用視頻 ID 和時間戳作為文件名
|
122 |
filename = f"youtube_{video_id}_{int(time.time())}"
|
123 |
temp_dir = tempfile.mkdtemp()
|
124 |
+
|
125 |
try:
|
126 |
# 準備下載路徑
|
127 |
temp_filepath_tmpl = os.path.join(download_dir, f"{filename}.%(ext)s")
|
128 |
+
|
129 |
# 設置 yt-dlp 選項
|
130 |
ydl_opts = {
|
131 |
'format': 'bestaudio/best',
|
|
|
139 |
}],
|
140 |
'ffmpeg_location': shutil.which("ffmpeg"),
|
141 |
}
|
142 |
+
|
143 |
# 檢查 ffmpeg
|
144 |
if not ydl_opts['ffmpeg_location']:
|
145 |
print("Warning: ffmpeg not found... / 警告:找不到 ffmpeg...")
|
146 |
+
|
147 |
# 如果已經有 API 信息,使用它
|
148 |
duration = api_info["duration"] if api_info else None
|
149 |
title = api_info["title"] if api_info else "Unknown"
|
150 |
+
|
151 |
# 下載音頻
|
152 |
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
153 |
# 如果沒有 API 信息,從 yt-dlp 獲取
|
|
|
158 |
else:
|
159 |
# 有 API 信息,直接下載
|
160 |
ydl.download([f"https://www.youtube.com/watch?v={video_id}"])
|
161 |
+
|
162 |
# 確定最終文件路徑
|
163 |
final_filepath = os.path.join(download_dir, f"{filename}.mp3")
|
164 |
+
|
165 |
# 檢查文件是否存在
|
166 |
if os.path.exists(final_filepath):
|
167 |
print(f"YouTube audio downloaded: {final_filepath}")
|
|
|
170 |
else:
|
171 |
# 嘗試查找可能的文件
|
172 |
potential_files = [
|
173 |
+
os.path.join(download_dir, f)
|
174 |
+
for f in os.listdir(download_dir)
|
175 |
if f.startswith(filename) and f.endswith(".mp3")
|
176 |
]
|
177 |
if potential_files:
|
|
|
180 |
return downloaded_path, temp_dir, duration
|
181 |
else:
|
182 |
raise FileNotFoundError(f"Audio file not found after download in {download_dir}")
|
183 |
+
|
184 |
except Exception as e:
|
185 |
print(f"Error downloading YouTube audio: {e}")
|
186 |
if temp_dir and os.path.exists(temp_dir):
|
|
|
190 |
print(f"Error cleaning temp directory {temp_dir}: {cleanup_e}")
|
191 |
return None, None, None
|
192 |
|
193 |
+
def process_youtube_url(youtube_url, user_api_key=None):
|
194 |
"""處理 YouTube URL,獲取信息並下載音頻
|
195 |
+
|
196 |
Args:
|
197 |
youtube_url: YouTube 視頻 URL
|
198 |
+
user_api_key: 用戶提供的 API 金鑰(可選)
|
199 |
+
|
200 |
Returns:
|
201 |
tuple: (音頻文件路徑, 視頻信息)
|
202 |
"""
|
203 |
# 檢查 URL 是否有效
|
204 |
if not youtube_url or not youtube_url.strip():
|
205 |
return None, None
|
206 |
+
|
207 |
+
# 檢查是否在 Hugging Face Spaces 環境中
|
208 |
+
is_spaces = os.environ.get("SPACE_ID") is not None
|
209 |
+
|
210 |
+
# 如果提供了用戶 API 金鑰,設置它
|
211 |
+
if user_api_key:
|
212 |
+
set_api_key(user_api_key)
|
213 |
+
|
214 |
# 提取視頻 ID
|
215 |
video_id = extract_video_id(youtube_url)
|
216 |
if not video_id:
|
217 |
print(f"Invalid YouTube URL: {youtube_url}")
|
218 |
return None, None
|
219 |
+
|
220 |
# 檢查是否設置了 API 金鑰
|
221 |
if YOUTUBE_API_KEY:
|
222 |
# 使用 API 獲取視頻信息
|
223 |
video_info = get_video_info(video_id)
|
224 |
if not video_info:
|
225 |
print(f"Could not get video info from API for: {video_id}")
|
226 |
+
|
227 |
+
# 如果在 Spaces 環境中且沒有 API 信息,則不嘗試下載
|
228 |
+
if is_spaces:
|
229 |
+
raise ValueError("YouTube 下載在 Hugging Face Spaces 中需要有效的 API 金鑰。")
|
230 |
+
|
231 |
# 如果 API 失敗,嘗試直接下載
|
232 |
audio_path, temp_dir, duration = download_audio(video_id)
|
233 |
return audio_path, {"title": "Unknown", "duration": duration}
|
234 |
+
|
235 |
# 使用 API 信息下載音頻
|
236 |
audio_path, temp_dir, _ = download_audio(video_id, video_info)
|
237 |
return audio_path, video_info
|
238 |
else:
|
239 |
+
# 沒有 API 金鑰
|
240 |
+
if is_spaces:
|
241 |
+
# 在 Spaces 環境中需要 API 金鑰
|
242 |
+
raise ValueError("YouTube 下載在 Hugging Face Spaces 中需要 API 金鑰。請在上方的 'YouTube API Key Settings' 中輸入您的 API 金鑰。\n\nYouTube download in Hugging Face Spaces requires an API key. Please enter your API key in the 'YouTube API Key Settings' section above.")
|
243 |
+
|
244 |
+
# 本地環境,直接使用 yt-dlp
|
245 |
print("No YouTube API key set, using yt-dlp directly")
|
246 |
audio_path, temp_dir, duration = download_audio(video_id)
|
247 |
return audio_path, {"title": "Unknown", "duration": duration}
|
|
|
251 |
# 設置 API 金鑰(實際使用時應從環境變量或配置文件獲取)
|
252 |
api_key = "YOUR_API_KEY"
|
253 |
set_api_key(api_key)
|
254 |
+
|
255 |
# 測試 URL
|
256 |
test_url = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
|
257 |
+
|
258 |
# 處理 URL
|
259 |
audio_path, video_info = process_youtube_url(test_url)
|
260 |
+
|
261 |
if audio_path and video_info:
|
262 |
print(f"Downloaded: {audio_path}")
|
263 |
print(f"Video info: {video_info}")
|