yt-dlp Douyu: PhantomJS not found और रूम आईडी की त्रुटियाँ ठीक करें
प्रकाशन की तारीख
सुधार प्लगइन डाउनलोड करें douyu_signing.py
अगर yt-dlp से Douyu वीडियो डाउनलोड करते समय PhantomJS not found या लाइव स्ट्रीम रिकॉर्ड करते समय Unable to extract room id आता है, तो इस लेख का संगतता प्लगइन लोड करें। यह तीन समस्याएँ ठीक करता है: नए लाइव पेज से रूम आईडी निकालना, Deno से हस्ताक्षर की गणना करना और लाइव स्ट्रीम का URL सही तरह जोड़ना।
अगर आप अपने कंप्यूटर पर Deno और प्लगइन डायरेक्टरी सेट नहीं करना चाहते, तो Douyu का लिंक हमारे होम पेज के डाउनलोडर में डालकर ऑनलाइन टूल इस्तेमाल कर सकते हैं।
Douyu के लाइव और ऑन-डिमांड वीडियो की समस्याएँ जाँचते समय हमें लगातार तीन रुकावटें मिलीं। नए पेज में Next.js डेटा ब्लॉक आने से पुराना रेगुलर एक्सप्रेशन रूम आईडी नहीं पढ़ पा रहा था। रूम आईडी ठीक करने के बाद भी मूल एक्सट्रैक्टर हस्ताक्षर बनाने के लिए पुराने, अप्रचलित PhantomJS को सीधे बुला रहा था। फिर हस्ताक्षर सही बनने और रिज़ॉल्यूशन की सूची मिलने के बाद पता चला कि बने हुए FLV स्ट्रीम URL में /live/ डायरेक्टरी गायब थी। नीचे पहले समाधान के चरण हैं, फिर हर समस्या का कारण समझाया गया है।
त्रुटि संदेश और संबंधित चरण
| त्रुटि या लक्षण | समस्या कहाँ है |
|---|---|
Unable to extract room id |
नए लाइव पेज में Next.js डेटा स्ट्रीम है; पुराना $ROOM.room_id मिलान विफल हो जाता है |
PhantomJS not found |
मूल douyutv.py में PhantomJSwrapper को सीधे बुलाने वाला कोड है; केवल Deno इंस्टॉल करने से यह कॉल अपने आप नहीं बदलता |
फ़ॉर्मैट की सूची मिलती है (-F काम करता है), लेकिन डाउनलोड त्रुटि के साथ बंद हो जाता है |
API के लौटाए rtmp_url के अंत में स्लैश नहीं है; urljoin जोड़ते समय /live/ डायरेक्टरी हटा देता है |
The channel is not currently live |
रूम लाइव नहीं है; रूम की स्थिति के अनुसार प्रक्रिया रोकना सामान्य व्यवहार है |
The channel is auto-playing VODs |
रूम ऑन-डिमांड वीडियो बार-बार चला रहा है (videoLoop: 1); प्लगइन डेटा निकालने की प्रक्रिया रोक देता है |
समस्या ठीक करने के त्वरित चरण
1. वातावरण और प्लगइन फ़ाइल तैयार करें
सुनिश्चित करें कि टर्मिनल में yt-dlp, deno और ffmpeg उपलब्ध हैं। इस लेख के परीक्षणों में yt-dlp nightly 2026.10.29.232701, Deno 2.9.8 और FFmpeg 7.1.5 इस्तेमाल किए गए थे। प्लगइन yt-dlp के अंतर्निहित _search_nextjs_v13_data मेथड पर निर्भर है। अगर आपका संस्करण बहुत पुराना है, तो पहले yt-dlp अपडेट करें।
अपनी कार्यशील डायरेक्टरी में नीचे दिए ढाँचे के अनुसार फ़ोल्डर बनाएँ और लेख के अंत में दिया पूरा प्लगइन कोड douyu_signing.py में सहेजें:
plugins/
└── douyu/
└── yt_dlp_plugins/
└── extractor/
└── douyu_signing.py
आगे के सभी कमांड उस डायरेक्टरी में चलाएँ जिसमें plugins फ़ोल्डर है। --plugin-dirs ./plugins से प्लगइन लोड होता है; yt-dlp की इंस्टॉलेशन डायरेक्टरी का सोर्स कोड बदलने की ज़रूरत नहीं है।
2. लाइव स्ट्रीम की फ़ॉर्मैट सूची जाँचें
यह उदाहरण एक सार्वजनिक लाइव रूम का है। URL को अपने लक्षित रूम के पते से बदलें:
yt-dlp --ignore-config --plugin-dirs ./plugins --no-cache-dir -v -F "https://www.douyu.com/pigff"
लॉग में [DouyuTV+DenoSigning] दिखने का मतलब है कि प्लगइन सक्रिय है। रूम लाइव होने पर getH5Play API को अनुरोध भेजा जाता है और अलग-अलग रिज़ॉल्यूशन के फ़ॉर्मैट दिखते हैं। अगर The channel is not currently live आए, तो रूम अभी लाइव नहीं है। परीक्षण के लिए कोई ऐसा रूम चुनें जहाँ प्रसारण चल रहा हो।
3. मीडिया स्ट्रीम जाँचने के लिए 30 सेकंड रिकॉर्ड करें
फ़ॉर्मैट की सूची मिलना केवल यह बताता है कि हस्ताक्षर वाले API अनुरोध ने error: 0 लौटाया है। CDN का पता सही तरह जुड़ा है या नहीं, यह पुष्टि करने के लिए स्ट्रीम का डेटा वास्तव में लेना ज़रूरी है:
yt-dlp --ignore-config --plugin-dirs ./plugins --no-cache-dir --downloader ffmpeg --downloader-args "ffmpeg_i:-rw_timeout 15000000" --downloader-args "ffmpeg_o:-t 30" -f worst -o "douyu-live.%(ext)s" "https://www.douyu.com/pigff"
-f worst सबसे कम बिटरेट चुनता है ताकि परीक्षण फ़ाइल छोटी रहे। -t 30 रिकॉर्डिंग को 30 सेकंड तक सीमित करता है, और नेटवर्क पर पढ़ने-लिखने की समय-सीमा 15 सेकंड रखी गई है।
4. ऑन-डिमांड वीडियो और फ़ाइल जाँचें (वैकल्पिक)
यही प्लगइन Douyu के ऑन-डिमांड वीडियो (VOD) एक्सट्रैक्टर DouyuShowIE पर भी काम करता है। VOD जाँचते समय लॉग में [DouyuShow+DenoSigning] दिखाई देता है:
# List VOD formats
yt-dlp --ignore-config --plugin-dirs ./plugins --no-cache-dir -F "https://v.douyu.com/show/mPyq7oVNe5Yv1gLY"
# Download the VOD sample
yt-dlp --ignore-config --plugin-dirs ./plugins --no-cache-dir -f worst -o "douyu-video.%(ext)s" "https://v.douyu.com/show/mPyq7oVNe5Yv1gLY"
रिकॉर्ड की गई .flv या .mp4 फ़ाइल में खराब फ़्रेम हैं या नहीं और ऑडियो-वीडियो ट्रैक पूरे हैं या नहीं, यह जाँचने के लिए ffprobe और पूरी फ़ाइल की डिकोडिंग जाँच चलाएँ:
ffprobe -v error -show_entries stream=codec_name,codec_type,width,height:format=duration,size -of json "douyu-live.flv"
ffmpeg -nostdin -v error -xerror -i "douyu-live.flv" -map 0:v:0 -map "0:a:0?" -f null -
तीनों समस्याओं की तकनीकी जाँच
पहली समस्या: Next.js डेटा ब्लॉक आने से पुराना पैटर्न रूम आईडी नहीं पढ़ता
मूल एक्सट्रैक्टर पेज में $ROOM.room_id असाइनमेंट से रूम आईडी निकालता था। संबंधित त्रुटि yt-dlp की issue #16230 में दर्ज है। लेकिन Douyu के नए लाइव पेज, जैसे pigff उपनाम या 593229 नंबर वाले रूम में, रूम का मेटाडेटा अब Next.js App Router के स्क्रिप्ट डेटा ब्लॉक (__next_f.push) में रहता है।
पूरे पेज में बहुत सामान्य रेगुलर एक्सप्रेशन से room_id खोजने पर साइडबार या नीचे सुझाए गए किसी दूसरे रूम की आईडी मिल सकती है। इसलिए प्लगइन _search_nextjs_v13_data से डेटा स्ट्रीम पार्स करता है और केवल वर्तमान रूम की roomInfo.room डिक्शनरी पढ़ता है:
pigffउपनाम सही वास्तविक रूम आईडी24422में बदलता है।- हस्ताक्षर माँगने से पहले रूम की स्थिति जाँची जाती है।
show_status == 2होने पर तुरंत ऑफ़लाइन संदेश मिलता है, जैसे परीक्षण के समय बंद रूम593229के लिए।room_biz_all.videoLoop == 1होने पर बताया जाता है कि रूम वीडियो बार-बार चला रहा है। इससे अनावश्यक हस्ताक्षर गणना नहीं होती। - अगर पेज में Next.js डेटा ब्लॉक नहीं हैं और पुरानी संरचना है, तो प्रक्रिया अपने आप मूल
super()._real_extract(url)को दे दी जाती है।
दूसरी समस्या: मूल कोड में PhantomJSwrapper की सीधी कॉल
सही room_id मिलने के बाद एक्सट्रैक्टर को v, did, tt और sign पैरामीटर बनाने के लिए Douyu का ub98484234 हस्ताक्षर फ़ंक्शन चलाना होता है। yt-dlp का सुझाया Deno पहले से इंस्टॉल होने पर भी मूल douyutv.py का _calc_sign सीधे PhantomJSwrapper का इंस्टेंस बनाता है। इससे लाइव और VOD, दोनों में PhantomJS not found आता है।
प्लगइन _calc_sign को बदलता है, लेकिन हस्ताक्षर स्क्रिप्ट और अंतर्निहित CryptoJS MD5 प्राप्त करने का मूल तरीका रखता है। लाइव के लिए room_id और VOD के लिए window.$DATA के ROOM.point_id फ़ील्ड से निकाली गई वीडियो आईडी दी जाती है। पैरामीटर json.dumps से सुरक्षित रूप से एन्कोड किए जाते हैं और मानक इनपुट से एक ऐसी Deno उपप्रक्रिया को दिए जाते हैं जिसे पहुँच की कोई अनुमति नहीं दी गई है:
--no-prompt,--no-configऔर--no-remoteस्पष्ट रूप से दिए जाते हैं। फ़ाइलों, वातावरण के वेरिएबल या नेटवर्क तक पहुँच की अनुमति नहीं दी जाती। Deno का अनुमति मॉडल JavaScript API की पहुँच सीमित करता है; यह ऑपरेटिंग सिस्टम स्तर के सैंडबॉक्स का विकल्प नहीं है।- समय-सीमा 15 सेकंड है। लौटी हुई क्वेरी स्ट्रिंग में
signफ़ील्ड खाली न होने की जाँच के बाद पैरामीटर/lapi/live/getH5Play/{room_id}या VOD API को भेजे जाते हैं। परीक्षण में दोनों API नेerror: 0लौटाया।
तीसरी समस्या: urljoin से /live/ डायरेक्टरी हट जाती है
रूम आईडी और हस्ताक्षर ठीक होने के बाद -F कई बिटरेट वाले फ़ॉर्मैट दिखा रहा था। फिर भी FFmpeg से रिकॉर्डिंग का पहला प्रयास निकास कोड 8 (ffmpeg exited with code 8) के साथ विफल हुआ।
getH5Play के JSON उत्तर की जाँच से पता चला कि स्ट्रीम का पता दो हिस्सों में था: डायरेक्टरी का उपसर्ग rtmp_url और फ़ाइल नाम rtmp_live। rtmp_url का अंत /live पर था, उसके बाद स्लैश नहीं था। मूल कोड में urllib.parse.urljoin से सीधे जोड़ते समय live को अंतिम फ़ाइल नाम मानकर बदल दिया गया। Issue #11998 में भी /live/ न होने के कारण CDN से 403 मिलने का मामला दर्ज है:
urljoin('https://cdn.example/live', '24422.flv?sign=example')
# Incorrect result: https://cdn.example/24422.flv?sign=example
urljoin('https://cdn.example/live/', '24422.flv?sign=example')
# Correct result: https://cdn.example/live/24422.flv?sign=example
प्लगइन _extract_stream_formats में पहले data['rtmp_url'].rstrip('/') + '/' से rtmp_url के अंत में स्लैश लगाता है, फिर डेटा मूल फ़ॉर्मैट पार्सर को देता है। स्ट्रीम का फ़ाइल नाम और हस्ताक्षर वाले क्वेरी पैरामीटर जैसे के तैसे रहते हैं। सुधार के बाद लाइव रिकॉर्डिंग और VOD खंडों के डाउनलोड, दोनों में पूरा डेटा मिला और डिकोडिंग जाँच सफल रही।
पूरा प्लगइन कोड
नीचे का कोड plugins/douyu/yt_dlp_plugins/extractor/douyu_signing.py में सहेजें:
"""Run Douyu signing scripts in Deno without file, environment, or network access."""
import json
import shutil
import subprocess
import time
import urllib.parse
import uuid
from yt_dlp.extractor.douyutv import DouyuShowIE as UpstreamDouyuShowIE, DouyuTVIE as UpstreamDouyuTVIE
from yt_dlp.utils import (
ExtractorError,
UserNotLive,
int_or_none,
str_or_none,
traverse_obj,
unescapeHTML,
url_or_none,
urlencode_postdata,
)
class _DenoSigning:
def _calc_sign(self, sign_func, video_id, room_id):
deno = shutil.which('deno')
if not deno:
raise ExtractorError('Deno is required for Douyu signing', expected=True)
script = ('globalThis.window=globalThis;' + self._get_cryptojs_md5(video_id) + ';' + sign_func
+ ';console.log(ub98484234(' + ','.join(json.dumps(value) for value in
(str(room_id), uuid.uuid4().hex, str(round(time.time())))) + '))')
try:
result = subprocess.run([deno, 'run', '--no-prompt', '--no-config', '--no-remote', '-'],
input=script, text=True, capture_output=True, timeout=15)
except (OSError, subprocess.TimeoutExpired) as exc:
raise ExtractorError('Douyu signing runtime failed', cause=exc) from exc
lines = result.stdout.strip().splitlines()
fields = urllib.parse.parse_qs(lines[-1]) if lines and result.returncode == 0 else {}
if not fields.get('sign'):
raise ExtractorError('Unable to sign Douyu request with Deno')
return {key: values[0] for key, values in fields.items()}
class DouyuShowIE(UpstreamDouyuShowIE, plugin_name='DenoSigning'):
_calc_sign = _DenoSigning._calc_sign
class DouyuTVIE(UpstreamDouyuTVIE, plugin_name='DenoSigning'):
_calc_sign = _DenoSigning._calc_sign
def _extract_stream_formats(self, stream_formats):
normalized = []
for response in stream_formats:
data = response.get('data')
if isinstance(data, dict) and isinstance(data.get('rtmp_url'), str):
# Treat /live as a directory so urljoin preserves it.
response = {**response, 'data': {
**data, 'rtmp_url': data['rtmp_url'].rstrip('/') + '/',
}}
normalized.append(response)
return super()._extract_stream_formats(normalized)
def _real_extract(self, url):
video_id = self._match_id(url)
webpage = self._download_webpage(url, video_id)
room = traverse_obj(
self._search_nextjs_v13_data(webpage, video_id, fatal=False),
(..., 'roomInfo', 'room', {dict}), get_all=False)
if not room:
# Let upstream handle older pages without guessing IDs from recommendations or URLs.
return super()._real_extract(url)
room_id = str_or_none(room.get('room_id'))
if not room_id or not room_id.isascii() or not room_id.isdecimal() or int(room_id) <= 0:
raise ExtractorError('Unable to extract room id')
if traverse_obj(room, ('room_biz_all', 'videoLoop', {int_or_none})) == 1:
raise UserNotLive('The channel is auto-playing VODs', video_id=video_id)
if int_or_none(room.get('show_status')) == 2:
raise UserNotLive(video_id=video_id)
# New pages omit the signing function; reuse upstream script fetching and format parsing.
sign_func = self._get_sign_func(room_id, video_id)
form_data = {'rate': 0, **self._calc_sign(sign_func, video_id, room_id)}
stream_formats = [self._download_json(
f'https://www.douyu.com/lapi/live/getH5Play/{room_id}', video_id,
note='Downloading livestream format', data=urlencode_postdata(form_data))]
for rate_id in traverse_obj(stream_formats[0], ('data', 'multirates', ..., 'rate')):
if rate_id != traverse_obj(stream_formats[0], ('data', 'rate')):
form_data['rate'] = rate_id
stream_formats.append(self._download_json(
f'https://www.douyu.com/lapi/live/getH5Play/{room_id}', video_id,
note=f'Downloading livestream format {rate_id}', data=urlencode_postdata(form_data)))
return {
'id': room_id,
'display_id': video_id,
'formats': self._extract_stream_formats(stream_formats),
'is_live': True,
**traverse_obj(room, {
'title': ('room_name', {str}, {unescapeHTML}),
'description': ('show_details', {str}),
'uploader': ('nickname', {str}),
'thumbnail': ('coverSrc', {url_or_none}),
}),
}