{"record":{"id":"164d96264e3393b3","repo":"ytdl-org/youtube-dl","slug":"said-s","errorCode":null,"errorMessage":"% said: %s","messagePattern":"% said: (.+?)","errorType":"exception","errorClass":"ExtractorError","httpStatus":null,"severity":"error","filePath":"youtube_dl/extractor/uplynk.py","lineNumber":42,"sourceCode":"        'params': {\n            # m3u8 download\n            'skip_download': True,\n        },\n    }\n\n    def _extract_uplynk_info(self, uplynk_content_url):\n        path, external_id, video_id, session_id = re.match(UplynkIE._VALID_URL, uplynk_content_url).groups()\n        display_id = video_id or external_id\n        formats = self._extract_m3u8_formats(\n            'http://content.uplynk.com/%s.m3u8' % path,\n            display_id, 'mp4', 'm3u8_native')\n        if session_id:\n            for f in formats:\n                f['extra_param_to_segment_url'] = 'pbs=' + session_id\n        self._sort_formats(formats)\n        asset = self._download_json('http://content.uplynk.com/player/assetinfo/%s.json' % path, display_id)\n        if asset.get('error') == 1:\n            raise ExtractorError('% said: %s' % (self.IE_NAME, asset['msg']), expected=True)\n\n        return {\n            'id': asset['asset'],\n            'title': asset['desc'],\n            'thumbnail': asset.get('default_poster_url'),\n            'duration': float_or_none(asset.get('duration')),\n            'uploader_id': asset.get('owner'),\n            'formats': formats,\n        }\n\n    def _real_extract(self, url):\n        return self._extract_uplynk_info(url)\n\n\nclass UplynkPreplayIE(UplynkIE):\n    IE_NAME = 'uplynk:preplay'\n    _VALID_URL = r'https?://.*?\\.uplynk\\.com/preplay2?/(?P<path>ext/[0-9a-f]{32}/(?P<external_id>[^/?&]+)|(?P<id>[0-9a-f]{32}))\\.json'\n    _TEST = None","sourceCodeStart":24,"sourceCodeEnd":60,"githubUrl":"https://github.com/ytdl-org/youtube-dl/blob/956b8c585591b401a543e409accb163eeaaa1193/youtube_dl/extractor/uplynk.py#L24-L60","documentation":"Raised by the Uplynk extractor when the asset-info endpoint (content.uplynk.com/player/assetinfo/<path>.json) returns a JSON object with error==1; the site's own 'msg' text is propagated. It is an expected, site-side rejection (geo-block, expired asset, bad asset path) rather than a download bug. Note the format string is '% said: %s' — the leading IE name placeholder is missing its 's', so the literal message renders with a stray single '%' and the extractor name never appears.","triggerScenarios":"Calling _extract_uplynk_info() on an uplynk content URL whose assetinfo JSON contains {\"error\": 1, \"msg\": ...} — e.g. expired/revoked asset, wrong external id, or a CMS-side takedown.","commonSituations":"Extracting old uplynk.com or embed links whose assets were decommissioned; typo'd video id in the URL; regional restrictions enforced by the uplynk backend.","solutions":["Verify the asset still plays in a browser at the same content.uplynk.com URL; if it 404s there, the video is gone and no code fix applies.","Confirm the path/external_id parsed from the URL matches the live embed (the regex groups feed the assetinfo request directly).","If maintaining the extractor, fix the format typo: '%s said: %s' so the message includes the extractor name and renders correctly.","Catch ExtractorError with expected=True and surface asset['msg'] to the user instead of retrying."],"exampleFix":"// before (uplynk.py:42)\nraise ExtractorError('% said: %s' % (self.IE_NAME, asset['msg']), expected=True)\n// after\nraise ExtractorError('%s said: %s' % (self.IE_NAME, asset['msg']), expected=True)","handlingStrategy":"try-catch","validationCode":"import json, urllib.request\npath = '...'  # asset path from URL\nreq = urllib.request.Request('http://content.uplynk.com/player/assetinfo/%s.json' % path)\nasset = json.load(urllib.request.urlopen(req))\nif asset.get('error') == 1:\n    print('asset rejected:', asset.get('msg'))  # skip before invoking the extractor","typeGuard":null,"tryCatchPattern":"from youtube_dl.utils import ExtractorError\ntry:\n    ydl.extract_info(url)\nexcept ExtractorError as e:\n    if e.expected and 'said:' in str(e):\n        log_skip(url, str(e))  # site-side rejection: permanent, do not retry\n    else:\n        raise","preventionTips":["Treat 'X said:' messages as permanent site rejections; never retry them.","Pre-check the assetinfo JSON for error==1 before extracting.","Track skip reasons per URL so batch jobs stay resumable."],"tags":["api","json","site-side-rejection","malformed-format-string"],"backgroundTag":null,"analyzedSha":"956b8c585591b401a543e409accb163eeaaa1193","analyzedAt":"2026-08-14T18:59:47.863Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}