ytdlp.online
Home Tampermonkey

Tutte le soluzioni

In questa pagina

yt-dlp e Douyu: correggere PhantomJS not found e gli errori dell’ID stanza

Data di pubblicazione

Scarica il plugin correttivo douyu_signing.py

Se yt-dlp segnala PhantomJS not found durante il download di un video Douyu o Unable to extract room id durante la registrazione di una diretta, puoi caricare il plugin di compatibilità fornito in questo articolo. Il plugin risolve tre problemi: l’estrazione dell’ID della stanza dalle nuove pagine, il calcolo della firma con Deno e la composizione dell’URL del flusso in diretta.

Se preferisci evitare la configurazione locale di Deno e della directory dei plugin, incolla il link Douyu nel downloader della nostra pagina iniziale per usare lo strumento online.

Analizzando i problemi con le dirette e i video on demand di Douyu, abbiamo incontrato tre ostacoli consecutivi. Il passaggio delle pagine ai blocchi dati Next.js impediva alla vecchia espressione regolare di trovare l’ID della stanza. Dopo questa correzione, l’estrattore continuava a richiamare direttamente PhantomJS, ormai obsoleto, per calcolare la firma. Una volta ottenuta una firma valida e l’elenco delle risoluzioni, nell’URL assemblato del flusso FLV mancava ancora la directory /live/. Di seguito trovi prima la procedura e poi l’analisi delle singole cause.

Sintomi e punti di errore

Messaggio o comportamento Punto del problema
Unable to extract room id Le nuove pagine delle dirette usano un flusso dati Next.js; la precedente ricerca di $ROOM.room_id non trova corrispondenze
PhantomJS not found Il file upstream douyutv.py richiama direttamente PhantomJSwrapper; installare Deno non sostituisce automaticamente questa chiamata
I formati vengono elencati (-F funziona), ma il download termina con un errore Il campo rtmp_url restituito dall’API non ha la barra finale; durante la composizione, urljoin elimina la directory /live/
The channel is not currently live La stanza è offline; l’interruzione corrisponde al normale controllo dello stato
The channel is auto-playing VODs La stanza riproduce video on demand in loop (videoLoop: 1); il plugin interrompe l’estrazione

Procedura rapida di correzione

1. Preparare l’ambiente e il file del plugin

Assicurati che yt-dlp, deno e ffmpeg siano disponibili nel terminale. I test descritti qui sono stati eseguiti con yt-dlp nightly 2026.10.29.232701, Deno 2.9.8 e FFmpeg 7.1.5. Il plugin dipende dal metodo _search_nextjs_v13_data integrato in yt-dlp: aggiorna prima yt-dlp se la tua versione è troppo vecchia.

Crea questa struttura di cartelle nella directory di lavoro e salva il codice completo del plugin in fondo all’articolo come douyu_signing.py:

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

Esegui tutti i comandi successivi dalla directory che contiene la cartella plugins. L’opzione --plugin-dirs ./plugins carica il plugin senza modificare il codice sorgente nella directory di installazione di yt-dlp.

2. Controllare l’elenco dei formati della diretta

Questo esempio usa una stanza pubblica. Sostituisci l’URL con quello della stanza che ti interessa:

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

La presenza di [DouyuTV+DenoSigning] nel log conferma che il plugin è attivo. Se la stanza sta trasmettendo, yt-dlp interroga l’API getH5Play e mostra i formati per le diverse risoluzioni. Il messaggio The channel is not currently live indica che la stanza non è in diretta: ripeti il test con una stanza che sta trasmettendo.

3. Registrare 30 secondi per verificare il flusso

Un elenco dei formati valido conferma soltanto che l’API ha restituito error: 0 per la richiesta firmata. Per verificare che l’indirizzo del CDN sia stato composto correttamente, occorre anche recuperare il flusso:

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"

L’opzione -f worst sceglie il bitrate più basso per ridurre le dimensioni del file di test. -t 30 limita la registrazione a 30 secondi, mentre il timeout di lettura e scrittura in rete è impostato a 15 secondi.

4. Verificare video on demand e file (facoltativo)

Lo stesso plugin copre anche DouyuShowIE, l’estrattore Douyu per i video on demand (VOD). Durante un test VOD, nel log compare [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"

Per verificare che il file .flv o .mp4 registrato non contenga fotogrammi danneggiati e che le tracce audio e video siano complete, usa ffprobe ed esegui una decodifica integrale:

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 -

Analisi tecnica dei tre problemi

Primo problema: i blocchi Next.js impediscono alla vecchia regex di trovare l’ID

L’estrattore upstream ricavava l’ID della stanza da un’assegnazione a $ROOM.room_id nel codice della pagina. Un errore di questo tipo è segnalato nella issue #16230. Nelle nuove pagine Douyu, per esempio quelle dell’alias pigff o della stanza numerica 593229, i metadati della stanza sono invece nei blocchi dati degli script dell’App Router di Next.js (__next_f.push).

Cercare room_id nell’intera pagina con una regex troppo generica può restituire l’ID di una stanza consigliata nella barra laterale o in fondo alla pagina. Il plugin analizza quindi il flusso con _search_nextjs_v13_data e legge soltanto il dizionario roomInfo.room relativo alla stanza corrente:

  • L’alias pigff viene risolto correttamente nell’ID reale 24422.
  • Lo stato della stanza viene controllato prima della richiesta di firma. Con show_status == 2, il plugin restituisce subito il messaggio di stanza offline, come per la stanza 593229 non in diretta durante il test. Con room_biz_all.videoLoop == 1, segnala che vengono riprodotti video in loop. In questo modo evita un calcolo della firma inutile.
  • Se la pagina non contiene blocchi dati Next.js e usa la vecchia struttura, il plugin torna automaticamente alla gestione upstream tramite super()._real_extract(url).

Secondo problema: la chiamata a PhantomJSwrapper è fissa nel codice

Dopo aver ottenuto la vera room_id, l’estrattore deve eseguire la funzione di firma ub98484234 fornita da Douyu per calcolare i quattro parametri v, did, tt e sign. Anche se Deno, consigliato da yt-dlp, è già installato, _calc_sign nel file upstream douyutv.py continua a istanziare direttamente PhantomJSwrapper. Ne deriva l’errore PhantomJS not found, sia per le dirette sia per i VOD.

Il plugin ridefinisce _calc_sign mantenendo la logica upstream per recuperare lo script di firma e la dipendenza CryptoJS MD5 integrata. Per le dirette viene passato room_id, mentre per i VOD viene usato l’ID del video letto dal campo ROOM.point_id di window.$DATA. Gli argomenti vengono codificati in modo sicuro con json.dumps e inviati tramite input standard a un sottoprocesso Deno senza permessi concessi:

  • Vengono specificate esplicitamente le opzioni --no-prompt, --no-config e --no-remote. Non viene concesso accesso a file, variabili d’ambiente o rete. Il modello dei permessi di Deno limita l’accesso tramite le API JavaScript, ma non sostituisce una sandbox a livello di sistema operativo.
  • Il timeout è di 15 secondi. Il plugin verifica che la stringa di query restituita contenga un campo sign non vuoto prima di inviare i parametri a /lapi/live/getH5Play/{room_id} o all’API VOD. Nei test, entrambe le API hanno restituito error: 0.

Terzo problema: urljoin elimina la directory /live/

Dopo aver risolto l’ID della stanza e la firma, -F mostrava già i formati a più bitrate. Il primo tentativo di registrazione con FFmpeg, però, è ancora terminato con il codice di uscita 8 (ffmpeg exited with code 8).

La risposta JSON di getH5Play divide l’indirizzo del flusso in due parti: rtmp_url fornisce il prefisso della directory e rtmp_live il nome del file. Il valore rtmp_url termina con /live, senza barra finale. Quando il codice upstream usa direttamente urllib.parse.urljoin, live viene trattato come l’ultimo nome di file e sostituito. Anche la issue #11998 documenta risposte 403 del CDN dovute all’assenza di /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

In _extract_stream_formats, il plugin aggiunge prima la barra finale a rtmp_url con data['rtmp_url'].rstrip('/') + '/', poi passa i dati al parser dei formati upstream. Il nome del file del flusso e i parametri di firma rimangono invariati. Dopo la correzione, sia la registrazione delle dirette sia il download dei frammenti VOD hanno recuperato tutti i dati e superato il controllo di decodifica.


Codice completo del plugin

Salva il codice seguente come 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}),
            }),
        }
Strumenti yt-dlp
Visualizza tutti gli strumenti yt-dlp
Guida e Documentazione yt-dlp