ytdl-org/youtube-dl · error · ExtractorError

%s said: %s

Error message

%s said: %s

What it means

Raised by TVNowNewBaseIE._call_api when the apigw.tvnow.de endpoint returns a JSON payload containing a non-empty 'error' key. It is a passthrough of the provider's own error text (e.g. 'not entitled', 'expired token'), tagged with the extractor name. Expected error: the message comes from the service, not from a parse failure.

Source

Thrown at youtube_dl/extractor/tvnow.py:213

    def _real_extract(self, url):
        mobj = re.match(self._VALID_URL, url)
        base_url = re.sub(r'(?:shows|serien)', '_', mobj.group('base_url'))
        show, episode = mobj.group('show', 'episode')
        return self.url_result(
            # Rewrite new URLs to the old format and use extraction via old API
            # at api.tvnow.de as a loophole for bypassing premium content checks
            '%s/%s/%s' % (base_url, show, episode),
            ie=TVNowIE.ie_key(), video_id=mobj.group('id'))


class TVNowNewBaseIE(InfoExtractor):
    def _call_api(self, path, video_id, query={}):
        result = self._download_json(
            'https://apigw.tvnow.de/module/' + path, video_id, query=query)
        error = result.get('error')
        if error:
            raise ExtractorError(
                '%s said: %s' % (self.IE_NAME, error), expected=True)
        return result


r"""
TODO: new apigw.tvnow.de based version of TVNowIE. Replace old TVNowIE with it
when api.tvnow.de is shut down. This version can't bypass premium checks though.
class TVNowIE(TVNowNewBaseIE):
    _VALID_URL = r'''(?x)
                    https?://
                        (?:www\.)?tvnow\.(?:de|at|ch)/
                        (?:shows|serien)/[^/]+/
                        (?:[^/]+/)+
                        (?P<display_id>[^/?$&]+)-(?P<id>\d+)
                    '''

    _TESTS = [{
        # episode with annual navigation

View on GitHub (pinned to 956b8c5855)

Solutions

  1. Read the '%s said:' payload — the provider text (e.g. 'Forbidden', 'not found') identifies the real cause.
  2. Verify the video id/URL opens on the tvnow website in the same region.
  3. Pass --cookies from a logged-in browser if the error indicates missing entitlement.
  4. Upgrade to yt-dlp for the maintained implementation of the apigw backend.
Defensive patterns

Strategy: try-catch

Validate before calling

import requests
r = requests.get('https://apigw.tvnow.de/module/' + path, params=query)
if r.json().get('error'):
    handle_provider_error(r.json()['error'])

Type guard

def tvnow_api_error(result: dict) -> bool:
    return bool(result.get('error'))

Try / catch

try:
    ydl.extract_info(url)
except ExtractorError as e:
    if 'said:' in str(e):
        log_provider_message(str(e))  # text after 'said:' is the provider's own error
    else:
        raise

Prevention

When it happens

Trigger: Any _download_json('https://apigw.tvnow.de/module/<path>', ...) whose response object has result['error'] set — e.g. requesting module/teaser or module/player with an invalid/expired item id, missing entitlement, or a stale session token in query.

Common situations: Using the newer tvnow.de URL scheme (routes handled by TVNowNewBaseIE subclasses), expired signed URLs, region restrictions reported as generic API errors, API contract changes after apigw rollout.

Related errors


AI-assisted analysis of ytdl-org/youtube-dl@956b8c5855 (2026-08-14). Data as JSON: /api/errors/24c7efed7d67b524. Report an issue: GitHub.