yt-dlp Rumble 403 修復:JSON 請求失敗後,改讀播放器資料
發布日期
如果使用 yt-dlp 下載 Rumble 影片時出現 Unable to download JSON metadata: HTTP Error 403: Forbidden,可以載入本文提供的相容性外掛。當 Rumble 的 embedJS/u3 JSON API 回傳 403 時,外掛會自動改為取得同一支影片的官方嵌入播放器頁面(/embed/),從中擷取內嵌的媒體資料並繼續下載。
如果不想在本機設定外掛或手動轉換連結,也可以直接使用本站已內建此修復方案的 Rumble 影片下載器。
先確認 403 發生在哪個步驟
Rumble 下載過程中的 403 可能出現在三個不同階段,本文外掛專門處理第二種:
| 錯誤訊息 | 遭到攔截的請求 | 處理方式 |
|---|---|---|
Unable to download webpage: HTTP Error 403 |
觀看頁面的 HTML 被攔截(參見 issue #15089) | 使用官方 oEmbed API 取得 /embed/ 網址,並搭配 --impersonate firefox |
Unable to download JSON metadata: HTTP Error 403 |
embedJS/u3 中繼資料 API 被攔截(參見 issue #17496) |
適用本文外掛:自動改讀 /embed/ 頁面的內嵌資料 |
| 已列出格式,但下載片段或 MP4 時出現 403 | CDN 媒體串流請求遭到拒絕 | 檢查代理伺服器、請求標頭,或改選其他畫質的格式 |
快速修復步驟
1. 儲存外掛檔案
在工作目錄中建立以下資料夾結構,並將文末完整外掛程式碼儲存為 rumble_player.py,不需要修改 yt-dlp 原始碼:
plugins/
└── rumble/
└── yt_dlp_plugins/
└── extractor/
└── rumble_player.py
2. 取得影片的官方 /embed/ 網址
Rumble 觀看頁面網址中的 ID(例如 v6xxxx)與嵌入播放器的 ID(例如 vb0ofn)並不相同,不能直接用觀看頁面的 ID 組合網址。如果只有一般的觀看頁面連結,可以先呼叫 Rumble 官方公開的 oEmbed API,取得實際的 /embed/ 網址:
curl -fsSG --data-urlencode "url=https://rumble.com/your-video.html" "https://rumble.com/api/Media/oembed.json"
在回傳 JSON 的 html 欄位中找到 <iframe src="https://rumble.com/embed/xxxx/...",取出其中的 https://rumble.com/embed/xxxx/ 即可。
如果 oEmbed 回傳 403,請先安裝 curl_cffi,再用 Firefox 指紋重試。請將下方指令中的範例網址也換成完整的影片觀看頁網址。
python -m pip install curl_cffi
python -c "import sys; from curl_cffi import requests; r = requests.get('https://rumble.com/api/Media/oembed.json', params=dict(url=sys.argv[1]), impersonate='firefox', timeout=15); r.raise_for_status(); print(r.text)" "https://rumble.com/your-video.html"
如果重試仍失敗,可以用線上下載器自動轉換連結。
3. 列出可用格式並確認備援路徑
在包含 plugins 資料夾的目錄下執行以下命令。實際使用時,請替換為目標影片的 /embed/ 網址:
yt-dlp --ignore-config --plugin-dirs ./plugins --no-cache-dir -v -F "https://rumble.com/embed/vb0ofn/"
記錄中出現 [RumbleEmbed+PlayerFallback],表示外掛已載入:
- 如果 JSON API 回傳 403,接著會看到一筆
Downloading player webpage記錄,表示備援路徑已啟動。之後顯示格式清單,才能確認資料解析成功。 - 如果目前的網路環境中 JSON API 本身回傳 200,則直接使用原有流程,不會額外請求播放器頁面。
4. 下載影片並驗證解碼
從 -F 列出的格式中選擇一個格式 ID(例如 mp4-360p)進行下載:
yt-dlp --ignore-config --plugin-dirs ./plugins --no-cache-dir -v --no-playlist -f mp4-360p -o "rumble-test.%(ext)s" "https://rumble.com/embed/vb0ofn/"
實測觸發備援路徑時的完整終端記錄如下:
[RumbleEmbed+PlayerFallback] vb0ofn: Downloading JSON metadata
[RumbleEmbed+PlayerFallback] vb0ofn: Downloading player webpage
[info] vb0ofn: Downloading 1 format(s): mp4-360p
[download] 100% of 7.85MiB in 00:01:20 at 99.48KiB/s
若要確認下載後的 MP4 容器與影音串流是否完整、沒有損壞,可使用 ffprobe 和 ffmpeg 檢查:
ffprobe -v error -show_entries stream=codec_name,codec_type,width,height:format=duration,size -of json "rumble-test.mp4"
ffmpeg -nostdin -v error -xerror -i "rumble-test.mp4" -map 0:v:0 -map "0:a:0?" -f null -
技術原理:為什麼播放器頁面能避開 JSON 403?
1. 重用同一網站的內嵌資料
RumbleEmbedIE 預設直接請求 https://rumble.com/embedJS/u3/?request=video&ver=2&v=...,取得 JSON 中繼資料。當 Cloudflare 或網站的存取管制單獨攔截這個 API,並回傳 HTTP 403 時,擷取流程便會中斷。
但開啟官方嵌入頁面 https://rumble.com/embed/<video_id>/ 時,網頁指令碼中已經內嵌了一份結構相同的影片設定物件,包含 ua 各畫質串流網址、標題、時長等欄位。只要擷取這個物件並作為 _download_json 的回傳值,yt-dlp 後續的格式解析程式碼就能照常執行,不必修改。
2. 移除非標準 JSON 欄位(loaded: h())
嵌入頁面中的資料物件並不是嚴格的 JSON 字面值。它的末尾帶有執行階段的函式呼叫 loaded: h(),形式如下:
u.f["vb0ofn"] = {"ua": {"mp4": {"360": {"url": "https://..."}}}, "title": "...", loaded: h()};
如果直接交給 JSON 解析器,h() 會造成語法錯誤。因此外掛透過 _search_json 的 transform_source 掛鉤,在解析前用正規表示式移除末尾的 , loaded: xxx()。整個過程只處理記憶體中的文字,不執行網頁 JavaScript:
- 精確比對:只擷取鍵名與目前
video_id完全一致的物件(\b[\w$]+\.f\[\s*["']{video_id}["']\s*\]\s*=),並確認回傳值是字典,且包含非空的ua媒體串流欄位。 - 最小改動:只有在
_download_json捕捉到HTTPError,且狀態碼為403時,才會啟用備援路徑。遇到 401、404、429、500 等錯誤,或頁面中沒有有效物件時,會原樣拋出原本的例外。 - 線上服務的配套處理:我們的線上服務除了載入此外掛,還會自動呼叫 oEmbed API,將使用者輸入的觀看頁面網址轉為
/embed/網址。未指定--impersonate時,預設啟用firefox-135指紋。
完整外掛程式碼
將以下程式碼儲存為 plugins/rumble/yt_dlp_plugins/extractor/rumble_player.py。已在 Python 3.12+ 與 yt-dlp 2026.10.10+ 環境中驗證通過:
"""Reuse official player media data when Rumble's JSON endpoint is blocked."""
import re
from yt_dlp.extractor.rumble import RumbleEmbedIE as BaseRumbleEmbedIE
from yt_dlp.networking.exceptions import HTTPError
from yt_dlp.utils import ExtractorError
class RumbleEmbedIE(BaseRumbleEmbedIE, plugin_name='PlayerFallback'):
def _real_extract(self, url):
self._player_url = url
return super()._real_extract(url)
def _download_json(self, url_or_request, video_id, *args, **kwargs):
try:
return super()._download_json(url_or_request, video_id, *args, **kwargs)
except ExtractorError as error:
if not isinstance(error.cause, HTTPError) or error.cause.status != 403:
raise
webpage = self._download_webpage(self._player_url, video_id, note='Downloading player webpage')
# Parse this video's object and strip the runtime call without executing page scripts.
data = self._search_json(
rf'''\b[\w$]+\.f\[\s*["']{re.escape(video_id)}["']\s*\]\s*=\s*''',
webpage, 'player data', video_id,
fatal=False,
transform_source=lambda source: re.sub(r',\s*loaded\s*:\s*[\w$]+\(\s*\)\s*(?=})', '', source))
if not isinstance(data, dict) or not data.get('ua'):
raise error
return data