ytdlp.online

جميع الحلول

في هذه الصفحة

yt-dlp وDouyu: إصلاح PhantomJS not found وأخطاء معرف الغرفة

تاريخ النشر

تنزيل إضافة الإصلاح douyu_signing.py

إذا ظهر PhantomJS not found عند تنزيل فيديوهات Douyu باستخدام yt-dlp، أو واجهت Unable to extract room id عند تسجيل بث مباشر، يمكنك تحميل إضافة التوافق الواردة في هذا المقال. تعالج الإضافة ثلاثة أمور: استخراج معرف الغرفة من صفحات البث الجديدة، وتنفيذ التوقيع باستخدام Deno، وتركيب عنوان تدفق البث المباشر.

إذا كنت تفضل تجنب إعداد Deno ومجلد الإضافات محليا، فالصق رابط Douyu في أداة التنزيل على الصفحة الرئيسية لاستخدام الخدمة عبر الإنترنت.

أثناء فحص أعطال البث المباشر والفيديو حسب الطلب في Douyu، واجهنا ثلاث عقبات متتالية. انتقلت الصفحة الجديدة إلى كتل بيانات Next.js، فلم يعد التعبير النمطي القديم يستخرج معرف الغرفة. وبعد إصلاحه، ظل المستخرج الأصلي يستدعي PhantomJS المهجور مباشرة لحساب التوقيع. وحين عمل التوقيع وظهرت صيغ الجودة المتاحة، وجدنا أن عنوان FLV الناتج يفتقد المجلد /live/. نبدأ بخطوات الإصلاح ثم نشرح أسباب هذه الأعطال.

أعراض العطل والمرحلة المسؤولة عنه

الخطأ أو العرض موضع العطل
Unable to extract room id تستخدم صفحة غرفة البث الجديدة تدفق بيانات Next.js، فلا ينجح نمط المطابقة القديم لـ $ROOM.room_id
PhantomJS not found يستدعي الملف الأصلي douyutv.py الصنف PhantomJSwrapper مباشرة؛ تثبيت Deno وحده لا يغير هذا الاستدعاء
تظهر الصيغ ويعمل -F، لكن التنزيل ينتهي بخطأ تعيد الواجهة 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. تعتمد الإضافة على الطريقة _search_nextjs_v13_data المضمنة في yt-dlp، لذا حدث yt-dlp أولا إذا كان إصدارك قديما جدا.

أنشئ بنية المجلدات التالية داخل مجلد العمل، ثم احفظ كود الإضافة الكامل باسم douyu_signing.py:

plugins/
└── douyu/
    └── yt_dlp_plugins/
        └── extractor/
            └── douyu_signing.py

نفذ الأوامر التالية من المجلد الذي يحتوي على plugins. يحمل الخيار --plugin-dirs ./plugins الإضافة دون تعديل الكود المصدري في مجلد تثبيت yt-dlp.

2. تحقق من قائمة صيغ البث المباشر

يستخدم المثال التالي غرفة بث عامة. استبدل عنوانها بعنوان الغرفة التي تريد اختبارها:

yt-dlp --ignore-config --plugin-dirs ./plugins --no-cache-dir -v -F "https://www.douyu.com/pigff"

ظهور [DouyuTV+DenoSigning] في السجل يؤكد عمل الإضافة. إذا كانت الغرفة تبث مباشرة، سيرسل المستخرج طلبا إلى واجهة getH5Play ويعرض الصيغ لمختلف مستويات الجودة. أما The channel is not currently live فتعني أن الغرفة لا تبث في تلك اللحظة؛ اختبر غرفة أخرى يكون بثها المباشر نشطا.

3. سجل 30 ثانية للتحقق من تدفق الوسائط

ظهور قائمة الصيغ لا يؤكد إلا أن الواجهة التي تستقبل التوقيع أعادت 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، وهو 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 الحقيقي، يحتاج المستخرج إلى استدعاء دالة التوقيع ub98484234 التي يوفرها Douyu لحساب المعاملات الأربعة v وdid وtt وsign. حتى لو كان Deno الذي يوصي به yt-dlp مثبتا، فإن _calc_sign في الملف الأصلي douyutv.py ينشئ كائنا من PhantomJSwrapper مباشرة. ينتج عن ذلك PhantomJS not found في كل من البث المباشر والفيديو حسب الطلب.

تستبدل الإضافة _calc_sign، مع الإبقاء على منطق المستخرج الأصلي لجلب سكربت التوقيع وتنفيذ MD5 المضمن في CryptoJS. تمرر room_id للبث المباشر، ومعرف الفيديو المستخرج من الحقل ROOM.point_id في window.$DATA للفيديو حسب الطلب. بعد ترميز المعاملات بأمان باستخدام json.dumps، ترسل السكربت عبر الإدخال القياسي إلى عملية فرعية من Deno دون أذونات وصول:

  • تمرر صراحة --no-prompt و--no-config و--no-remote، دون منح صلاحيات للملفات أو متغيرات البيئة أو الشبكة. يحد نموذج أذونات Deno من الوصول إلى واجهات JavaScript؛ ولا يحل محل العزل على مستوى نظام التشغيل.
  • تضبط مهلة من 15 ثانية وتتحقق من أن سلسلة الاستعلام الناتجة تحتوي على حقل sign غير فارغ، ثم ترسلها إلى /lapi/live/getH5Play/{room_id} أو واجهة الفيديو حسب الطلب. أعادت الواجهتان error: 0 في الاختبارات.

العطل الثالث: حذف urljoin للمجلد /live/ يؤدي إلى فشل جلب البث

بعد إصلاح معرف الغرفة والتوقيع، أصبح -F يعرض صيغ البث بمعدلات بت مختلفة. ومع ذلك، فشلت أول محاولة تسجيل باستخدام FFmpeg برمز الخروج 8 (ffmpeg exited with code 8).

عند فحص JSON الذي أعادته getH5Play، وجدنا أن الواجهة تقسم عنوان البث إلى جزأين: بادئة المجلد rtmp_url واسم الملف rtmp_live. كانت rtmp_url تنتهي بـ /live دون شرطة مائلة أخيرة. وعند دمج الجزأين مباشرة باستخدام urllib.parse.urljoin، تعامل الكود الأصلي مع live على أنه آخر اسم ملف فاستبدله. يوثق البلاغ #11998 أيضا استجابات 403 من CDN نتيجة فقدان /live/:

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