ytdlp.online

Wszystkie rozwiązania

Na tej stronie

yt-dlp i Douyu: naprawa PhantomJS not found oraz błędów ID pokoju

Data publikacji

Pobierz wtyczkę naprawczą douyu_signing.py

Jeśli podczas pobierania filmów z Douyu yt-dlp zgłasza PhantomJS not found albo podczas nagrywania transmisji pojawia się Unable to extract room id, możesz załadować wtyczkę zgodności z tego artykułu. Rozwiązuje ona trzy problemy: odczyt ID pokoju z nowych stron, obliczanie podpisu za pomocą Deno oraz składanie adresu strumienia na żywo.

Jeśli wolisz uniknąć lokalnej konfiguracji Deno i katalogu wtyczek, wklej link do Douyu w narzędziu do pobierania na stronie głównej i skorzystaj z wersji online.

Podczas diagnozowania błędów transmisji i materiałów VOD z Douyu natrafiliśmy na trzy kolejne przeszkody. Nowa strona używa bloków danych Next.js, więc dotychczasowe wyrażenie regularne nie odczytuje ID pokoju. Po naprawie tego etapu oryginalny ekstraktor nadal wywołuje przestarzały PhantomJS do obliczania podpisu. Gdy podpis już działa i dostępna jest lista formatów, złożony adres strumienia FLV gubi jeszcze katalog /live/. Najpierw przedstawiamy kroki naprawy, a następnie wyjaśniamy przyczyny.

Objawy i miejsce występowania błędu

Błąd lub objaw Etap awarii
Unable to extract room id Nowa strona pokoju używa strumienia danych Next.js, więc dawny wzorzec $ROOM.room_id nie znajduje dopasowania
PhantomJS not found Oryginalny douyutv.py wywołuje bezpośrednio PhantomJSwrapper; samo zainstalowanie Deno nie zmienia tego wywołania
Formaty są dostępne (-F działa), ale pobieranie kończy się błędem Wartość rtmp_url zwrócona przez API nie ma końcowego ukośnika, przez co urljoin usuwa katalog /live/ przy składaniu adresu
The channel is not currently live Pokój nie nadaje na żywo; przerwanie ekstrakcji w takim stanie jest prawidłowe
The channel is auto-playing VODs Pokój odtwarza materiały VOD w pętli (videoLoop: 1); wtyczka przerywa ekstrakcję

Jak zastosować poprawkę

1. Przygotuj środowisko i plik wtyczki

Upewnij się, że yt-dlp, deno i ffmpeg są zainstalowane i dostępne w terminalu. Testowaliśmy z yt-dlp nightly 2026.10.29.232701, Deno 2.9.8 oraz FFmpeg 7.1.5. Wtyczka korzysta z metody _search_nextjs_v13_data wbudowanej w yt-dlp. Jeśli lokalna wersja jest zbyt stara, najpierw ją zaktualizuj.

Utwórz poniższą strukturę folderów w katalogu roboczym i zapisz pełny kod wtyczki jako douyu_signing.py:

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

Wszystkie kolejne polecenia uruchamiaj w katalogu zawierającym folder plugins. Opcja --plugin-dirs ./plugins ładuje wtyczkę bez zmieniania kodu źródłowego w katalogu instalacyjnym yt-dlp.

2. Sprawdź listę formatów transmisji

Poniższy przykład używa publicznego pokoju transmisji. Zastąp URL adresem pokoju, który chcesz sprawdzić:

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

Oznaczenie [DouyuTV+DenoSigning] w logu potwierdza działanie wtyczki. Jeśli pokój nadaje na żywo, ekstraktor wywoła API getH5Play i wyświetli formaty poszczególnych jakości. Komunikat The channel is not currently live oznacza, że w tej chwili nie ma transmisji. Wybierz wtedy inny pokój, który nadaje na żywo.

3. Nagraj 30 sekund, aby sprawdzić strumień

Sama lista formatów potwierdza jedynie, że API przyjmujące podpis zwróciło error: 0. Aby upewnić się, że adres CDN jest poprawnie złożony, trzeba jeszcze faktycznie pobrać dane strumienia:

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"

Opcja -f worst wybiera najniższą przepływność, aby zmniejszyć rozmiar pliku testowego. -t 30 ogranicza nagranie do 30 sekund, a limit czasu odczytu i zapisu w sieci wynosi 15 sekund.

4. Sprawdź materiały VOD i pliki (opcjonalnie)

Ta sama wtyczka obsługuje też ekstraktor materiałów na żądanie Douyu, DouyuShowIE. Podczas testowania filmu VOD w logu pojawi się [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"

Aby sprawdzić, czy zapisany plik .flv lub .mp4 zawiera uszkodzone klatki oraz czy ścieżki obrazu i dźwięku są kompletne, uruchom ffprobe i dekodowanie całego pliku:

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 -

Analiza techniczna trzech usterek

Pierwsza usterka: po przejściu na dane Next.js wyrażenie regularne nie odczytuje ID pokoju

Oryginalny ekstraktor odczytywał ID pokoju z instrukcji przypisania do $ROOM.room_id w kodzie strony. Powiązany błąd opisuje zgłoszenie #16230. Na nowych stronach Douyu, zarówno tych z własnym aliasem, takim jak pigff, jak i z numerycznym ID, takim jak 593229, metadane pokoju zostały przeniesione do bloków danych Next.js App Router (__next_f.push).

Wyszukiwanie room_id szerokim wyrażeniem regularnym na całej stronie łatwo może zwrócić ID polecanego pokoju z paska bocznego lub dołu strony. Wtyczka używa więc _search_nextjs_v13_data do parsowania strumienia danych i odczytuje wyłącznie słownik roomInfo.room reprezentujący bieżący pokój:

  • Poprawnie rozwiązuje alias pigff do rzeczywistego ID 24422.
  • Sprawdza stan pokoju przed żądaniem podpisu. Gdy show_status == 2, od razu zgłasza brak transmisji, tak jak dla pokoju 593229. Gdy room_biz_all.videoLoop == 1, informuje o odtwarzaniu filmów w pętli. Pozwala to uniknąć niepotrzebnego obliczania podpisu.
  • Jeśli strona nie zawiera bloków danych Next.js i ma dawną strukturę, wtyczka automatycznie przekazuje jej obsługę do oryginalnego super()._real_extract(url).

Druga usterka: bezpośrednie wywołanie PhantomJSwrapper

Po uzyskaniu właściwego room_id ekstraktor musi wywołać dostarczaną przez Douyu funkcję podpisującą ub98484234, aby obliczyć cztery parametry: v, did, tt i sign. Nawet jeśli zalecany przez yt-dlp Deno jest już zainstalowany, _calc_sign w oryginalnym pliku douyutv.py nadal bezpośrednio tworzy instancję PhantomJSwrapper. Powoduje to błąd PhantomJS not found zarówno dla transmisji, jak i materiałów VOD.

Wtyczka zastępuje _calc_sign, zachowując oryginalną logikę pobierania skryptu podpisującego i implementacji MD5 z CryptoJS. Dla transmisji przekazuje room_id, a dla VOD identyfikator filmu odczytany z pola ROOM.point_id obiektu window.$DATA. Bezpiecznie koduje argumenty za pomocą json.dumps, po czym przekazuje skrypt przez standardowe wejście do procesu potomnego Deno bez uprawnień dostępu:

  • Jawnie podaje --no-prompt, --no-config i --no-remote, bez przyznawania dostępu do plików, zmiennych środowiskowych ani sieci. Model uprawnień Deno ogranicza dostęp do API JavaScriptu; nie zastępuje izolacji na poziomie systemu operacyjnego.
  • Ustawia limit czasu na 15 sekund i sprawdza, czy zwrócony ciąg parametrów zawiera niepuste pole sign, zanim wyśle go do /lapi/live/getH5Play/{room_id} lub interfejsu VOD. W testach oba interfejsy zwróciły error: 0.

Trzecia usterka: urljoin usuwa katalog /live/ i pobieranie strumienia się nie udaje

Po naprawie ID pokoju i podpisu -F pokazywało już formaty o różnych przepływnościach. Mimo to pierwsza próba nagrania przez FFmpeg zakończyła się kodem wyjścia 8 (ffmpeg exited with code 8).

Analiza JSON-a zwróconego przez getH5Play pokazała, że API dzieli adres na prefiks katalogu rtmp_url i nazwę pliku rtmp_live. Wartość rtmp_url kończyła się na /live, bez końcowego ukośnika. Oryginalny kod łączył je bezpośrednio za pomocą urllib.parse.urljoin, przez co live było traktowane jako ostatnia nazwa pliku i zastępowane. Zgłoszenie #11998 również opisuje odpowiedzi 403 z CDN spowodowane brakiem /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

W metodzie _extract_stream_formats wtyczka najpierw zapewnia końcowy ukośnik w rtmp_url (data['rtmp_url'].rstrip('/') + '/'), a następnie przekazuje dane do oryginalnego parsera. Nazwa pliku i parametry podpisu w adresie pozostają bez zmian. Po tej poprawce zarówno nagrywanie transmisji, jak i pobieranie fragmentów VOD zakończyły się powodzeniem i przeszły kontrolę dekodowania.


Pełny kod wtyczki

Zapisz poniższy kod jako 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}),
            }),
        }
Narzędzia yt-dlp
Zobacz wszystkie narzędzia yt-dlp
Pomoc i dokumentacja yt-dlp