ytdlp.online
ہوم Tampermonkey

تمام حل

اس صفحے پر

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 کا URL درست جڑنے کی تصدیق کے لیے اسٹریم کا ڈیٹا بھی واقعی حاصل کرنا ضروری ہے:

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 کے ویڈیو آن ڈیمانڈ ایکسٹریکٹر DouyuShowIE کو بھی سنبھالتا ہے۔ ایسی ویڈیو ٹیسٹ کرتے وقت لاگ میں [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 test 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 کو ویلیو دینے والی عبارت سے روم آئی ڈی نکالتا تھا۔ اس سے متعلق ایرر ایشو #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 ملنے کے بعد ایکسٹریکٹر کو Douyu کا دیا ہوا سگنیچر فنکشن ub98484234 چلا کر چار پیرامیٹرز v، did، tt اور sign بنانے ہوتے ہیں۔ yt-dlp کا تجویز کردہ Deno نصب ہونے کے باوجود، اصل douyutv.py میں _calc_sign اب بھی براہ راست PhantomJSwrapper بناتا ہے۔ اس سے لائیو اور آن ڈیمانڈ دونوں کے لیے PhantomJS not found آتا ہے۔

پلگ ان _calc_sign کو بدلتا ہے، لیکن سگنیچر اسکرپٹ اور شامل شدہ CryptoJS MD5 حاصل کرنے کی اصل منطق برقرار رکھتا ہے۔ لائیو کے لیے room_id اور آن ڈیمانڈ کے لیے window.$DATA کے ROOM.point_id فیلڈ سے نکالی گئی ویڈیو آئی ڈی پاس کی جاتی ہے۔ پیرامیٹرز کو json.dumps سے محفوظ طریقے سے انکوڈ کرنے کے بعد اسکرپٹ اسٹینڈرڈ اِن پٹ کے ذریعے Deno کے ایک ایسے سب پروسیس کو دیا جاتا ہے جسے رسائی کی اجازتیں نہیں دی گئیں:

  • --no-prompt، --no-config اور --no-remote واضح طور پر پاس کیے جاتے ہیں، اور فائلوں، ماحول کے متغیرات یا نیٹ ورک کی اجازت نہیں دی جاتی۔ Deno کا اجازتوں کا ماڈل JavaScript API تک رسائی محدود کرتا ہے؛ یہ آپریٹنگ سسٹم کی سطح کے سینڈباکس کا متبادل نہیں۔
  • 15 سیکنڈ کا ٹائم آؤٹ رکھا جاتا ہے۔ واپس آنے والی کوئری سٹرنگ میں غیر خالی sign فیلڈ کی تصدیق کے بعد اسے /lapi/live/getH5Play/{room_id} یا آن ڈیمانڈ API کو بھیجا جاتا ہے۔ ٹیسٹ میں دونوں APIs نے error: 0 لوٹایا۔

تیسری خرابی: urljoin کا /live/ ڈائریکٹری ہٹانا اسٹریم کو ناکام کر دیتا ہے

روم آئی ڈی اور سگنیچر درست ہونے کے بعد -F مختلف بٹ ریٹ کے فارمیٹس دکھا رہا تھا، لیکن FFmpeg سے پہلی ریکارڈنگ پھر بھی ایگزٹ کوڈ 8 (ffmpeg exited with code 8) کے ساتھ ناکام ہوئی۔

getH5Play کے واپس کیے ہوئے JSON کو دیکھنے پر معلوم ہوا کہ API اسٹریم کا پتہ دو حصوں میں دیتی ہے: ڈائریکٹری کا ابتدائی پتہ rtmp_url اور فائل کا نام rtmp_live۔ rtmp_url کا آخر /live تھا، مگر آخری سلیش موجود نہیں تھا۔ اصل کوڈ نے urllib.parse.urljoin سے انہیں براہ راست جوڑا، تو live کو آخری فائل کا نام سمجھ کر بدل دیا گیا۔ ایشو #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 میں پہلے rtmp_url کے آخر میں سلیش یقینی بناتا ہے (data['rtmp_url'].rstrip('/') + '/') اور پھر ڈیٹا اصل پارسر کو دیتا ہے۔ اسٹریم کی فائل کا نام اور سگنیچر کے کوئری پیرامیٹرز تبدیل نہیں ہوتے۔ اس درستگی کے بعد لائیو اسٹریم کی ریکارڈنگ اور آن ڈیمانڈ ویڈیو کے حصوں کا ڈاؤن لوڈ دونوں مکمل ہوئے اور ڈی کوڈنگ کی جانچ بھی کامیاب رہی۔


پلگ ان کا مکمل کوڈ

یہ کوڈ 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}),
            }),
        }
yt-dlp ٹولز
تمام yt-dlp ٹولز دیکھیں
yt-dlp مدد اور دستاویزات