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 navigationView on GitHub (pinned to 956b8c5855)
Solutions
- Read the '%s said:' payload — the provider text (e.g. 'Forbidden', 'not found') identifies the real cause.
- Verify the video id/URL opens on the tvnow website in the same region.
- Pass --cookies from a logged-in browser if the error indicates missing entitlement.
- 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
- Parse the provider text after 'said:' to classify entitlement vs. not-found vs. token expiry.
- Cache module/ API responses during development to avoid tripping rate limits.
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.