ytdlp.online

Tüm çözümler

Bu sayfada

yt-dlp Douyu hataları: PhantomJS not found ve oda kimliğini düzeltme

Yayın tarihi

Düzeltme eklentisini indir douyu_signing.py

yt-dlp ile Douyu videosu indirirken PhantomJS not found veya canlı yayın kaydederken Unable to extract room id hatası alıyorsanız bu yazıdaki uyumluluk eklentisini yükleyebilirsiniz. Eklenti üç sorunu birlikte çözer: yeni yayın sayfalarından oda kimliğinin çıkarılması, imzanın Deno ile hesaplanması ve canlı yayın URL’sinin doğru birleştirilmesi.

Deno’yu ve eklenti dizinini yerel olarak yapılandırmak istemiyorsanız Douyu bağlantısını ana sayfamızdaki indiriciye yapıştırıp çevrimiçi aracı kullanabilirsiniz.

Douyu canlı yayınları ve isteğe bağlı videolarındaki sorunları incelerken peş peşe üç engelle karşılaştık. Sayfaların Next.js veri bloklarına geçmesi, eski düzenli ifadenin oda kimliğini bulmasını engelliyordu. Oda kimliği düzeltildikten sonra asıl çıkarıcı, imza için kullanımdan kalkmış PhantomJS’i doğrudan çağırmaya devam ediyordu. İmza çalışıp çözünürlükler listelenmeye başladığında ise birleştirilen FLV yayın URL’sinde /live/ dizininin eksik olduğu görüldü. Önce uygulanacak adımları, ardından her sorunun nedenini açıklıyoruz.

Belirtiler ve hatanın oluştuğu aşamalar

Hata veya belirti Sorunun kaynağı
Unable to extract room id Yeni canlı yayın sayfası Next.js veri akışını kullanır; eski $ROOM.room_id eşleştirmesi sonuç vermez
PhantomJS not found Asıl douyutv.py dosyasında PhantomJSwrapper çağrısı sabit olarak yazılmıştır; sisteme Deno kurmak bu çağrıyı otomatik olarak değiştirmez
Formatlar listelenir (-F çalışır), ancak indirme hatayla sonlanır API’nin döndürdüğü rtmp_url değerinin sonunda eğik çizgi yoktur; urljoin birleştirme sırasında /live/ dizinini düşürür
The channel is not currently live Oda yayında değildir; oda durumu nedeniyle işlemin durdurulması normaldir
The channel is auto-playing VODs Oda isteğe bağlı videoları döngüde oynatır (videoLoop: 1); eklenti veri çıkarma işlemini durdurur

Hızlı çözüm adımları

1. Ortamı ve eklenti dosyasını hazırlayın

Terminalde yt-dlp, deno ve ffmpeg araçlarının kullanılabilir olduğundan emin olun. Bu yazıdaki testlerde yt-dlp nightly 2026.10.29.232701, Deno 2.9.8 ve FFmpeg 7.1.5 kullanıldı. Eklenti, yt-dlp’nin yerleşik _search_nextjs_v13_data metoduna bağımlıdır. Yerel sürümünüz çok eskiyse önce yt-dlp’yi güncelleyin.

Çalışma dizininizde aşağıdaki klasör yapısını oluşturun ve yazının sonundaki tam eklenti kodunu douyu_signing.py adıyla kaydedin:

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

Sonraki tüm komutları plugins klasörünü içeren dizinde çalıştırın. --plugin-dirs ./plugins seçeneği eklentiyi yükler; yt-dlp kurulum dizinindeki kaynak kodu değiştirmeniz gerekmez.

2. Canlı yayın formatlarını kontrol edin

Aşağıdaki örnek herkese açık bir yayın odasını kullanır. URL’yi hedef odanın adresiyle değiştirin:

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

Günlükte [DouyuTV+DenoSigning] görünmesi eklentinin etkin olduğunu gösterir. Oda canlı yayındaysa getH5Play API’si çağrılır ve farklı çözünürlüklerdeki formatlar listelenir. The channel is not currently live mesajı, odanın şu anda yayında olmadığını belirtir. Test için yayında olan başka bir oda seçin.

3. Medya akışını doğrulamak için 30 saniye kaydedin

Formatların listelenmesi yalnızca imzalı API isteğinin error: 0 döndürdüğünü gösterir. CDN adresinin doğru birleştirildiğini doğrulamak için akışın gerçekten alınması gerekir:

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, test dosyasını küçük tutmak için en düşük bit hızını seçer. -t 30, kaydı 30 saniyeyle sınırlar; ağ okuma ve yazma zaman aşımı ise 15 saniyedir.

4. İsteğe bağlı videoları ve dosyaları kontrol edin (isteğe bağlı)

Aynı eklenti, Douyu’nun isteğe bağlı video (VOD) çıkarıcısı DouyuShowIE için de çalışır. VOD testi sırasında günlükte [DouyuShow+DenoSigning] görünür:

# 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"

Kaydedilen .flv veya .mp4 dosyasında bozuk kare olup olmadığını ve ses ile video izlerinin eksiksizliğini kontrol etmek için ffprobe çalıştırabilir ve dosyanın tamamının kodunu çözebilirsiniz:

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 -

Üç sorunun teknik incelemesi

Birinci sorun: Next.js veri blokları eski oda kimliği eşleştirmesini bozuyor

Asıl çıkarıcı, oda kimliğini sayfadaki $ROOM.room_id atamasından okuyordu. İlgili hata yt-dlp projesinin issue #16230 kaydında yer alıyor. Ancak pigff takma adına veya 593229 numaralı odaya ait yeni Douyu sayfalarında oda meta verileri artık Next.js App Router’ın betik veri bloklarında (__next_f.push) bulunuyor.

Sayfanın tamamında çok genel bir düzenli ifadeyle room_id aramak, yan çubukta veya sayfanın altındaki önerilerde yer alan başka bir odanın kimliğini yakalayabilir. Eklenti bu yüzden veri akışını _search_nextjs_v13_data ile ayrıştırır ve yalnızca mevcut odayı temsil eden roomInfo.room sözlüğünü okur:

  • pigff takma adı, gerçek oda kimliği 24422 olarak doğru şekilde çözümlenir.
  • İmza istenmeden önce oda durumu kontrol edilir. show_status == 2 olduğunda, test sırasında yayında olmayan 593229 odasında olduğu gibi, doğrudan yayın olmadığı mesajı verilir. room_biz_all.videoLoop == 1 olduğunda ise odanın videoları döngüde oynattığı bildirilir. Böylece gereksiz imza hesaplaması yapılmaz.
  • Sayfada Next.js veri blokları yoksa, yani eski sayfa yapısı kullanılıyorsa, işlem otomatik olarak asıl super()._real_extract(url) metoduna bırakılır.

İkinci sorun: PhantomJSwrapper çağrısı kaynak kodda sabit

Gerçek room_id alındıktan sonra çıkarıcının v, did, tt ve sign parametrelerini hesaplamak için Douyu’nun sağladığı ub98484234 imza işlevini çalıştırması gerekir. yt-dlp’nin önerdiği Deno kurulu olsa bile asıl douyutv.py dosyasındaki _calc_sign, doğrudan PhantomJSwrapper örneği oluşturur. Bu da hem canlı yayınlarda hem VOD’larda PhantomJS not found hatasına yol açar.

Eklenti, imza betiğini ve yerleşik CryptoJS MD5 bağımlılığını alma mantığını koruyarak _calc_sign metodunu yeniden tanımlar. Canlı yayınlar için room_id, VOD’lar için window.$DATA içindeki ROOM.point_id alanından çıkarılan video kimliği geçirilir. Argümanlar json.dumps ile güvenli biçimde kodlanır ve standart girdi üzerinden izin verilmemiş bir Deno alt sürecine gönderilir:

  • --no-prompt, --no-config ve --no-remote açıkça belirtilir. Dosyalara, ortam değişkenlerine veya ağa erişim izni verilmez. Deno’nun izin modeli JavaScript API erişimini sınırlar; işletim sistemi düzeyinde bir yalıtım ortamının yerini tutmaz.
  • Zaman aşımı 15 saniyedir. Dönen sorgu dizesinde boş olmayan bir sign alanı bulunduğu doğrulandıktan sonra parametreler /lapi/live/getH5Play/{room_id} veya VOD API’sine gönderilir. Testlerde her iki API de error: 0 döndürdü.

Üçüncü sorun: urljoin, /live/ dizinini düşürüyor

Oda kimliği ve imza sorunları çözüldükten sonra -F farklı bit hızlarındaki formatları listeleyebiliyordu. Buna rağmen FFmpeg ile ilk kayıt denemesi yine çıkış kodu 8 ile başarısız oldu (ffmpeg exited with code 8).

getH5Play JSON yanıtı incelendiğinde akış adresinin iki parçaya ayrıldığı görüldü: dizin öneki rtmp_url ve dosya adı rtmp_live. rtmp_url, sonunda eğik çizgi olmadan /live ile bitiyordu. Asıl kod bu parçaları doğrudan urllib.parse.urljoin ile birleştirdiğinde live, son dosya adı olarak yorumlanıp değiştiriliyordu. Issue #11998 kaydında da /live/ eksikliği nedeniyle CDN’nin 403 döndürdüğü belgelenmiştir:

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

Eklenti, _extract_stream_formats içinde önce data['rtmp_url'].rstrip('/') + '/' kullanarak rtmp_url değerinin sonuna eğik çizgi ekler, ardından veriyi asıl format ayrıştırıcısına iletir. Akış dosyasının adı ve imza sorgu parametreleri değişmez. Düzeltmeden sonra hem canlı yayın kaydı hem VOD parça indirmesi verileri eksiksiz alabildi ve kod çözme kontrolünden geçti.


Tam eklenti kodu

Aşağıdaki kodu plugins/douyu/yt_dlp_plugins/extractor/douyu_signing.py dosyasına kaydedin:

"""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 Araçları
Tüm yt-dlp araçlarını görüntüleyin
yt-dlp Yardım ve Belgeler