{"record":{"id":"a4e327100c72f523","repo":"yt-dlp/yt-dlp","slug":"stream-is-offline-a4e327","errorCode":null,"errorMessage":"Stream is offline","messagePattern":"Stream is offline","errorType":"exception","errorClass":"ExtractorError","httpStatus":null,"severity":"error","filePath":"yt_dlp/extractor/picarto.py","lineNumber":53,"sourceCode":"        data = self._download_json(\n            'https://ptvintern.picarto.tv/ptvapi', channel_id, query={\n                'query': '''{\n  channel(name: \"%s\") {\n    adult\n    id\n    online\n    stream_name\n    title\n  }\n  getLoadBalancerUrl(channel_name: \"%s\") {\n    url\n  }\n}''' % (channel_id, channel_id),  # noqa: UP031\n            }, headers={'Accept': '*/*', 'Content-Type': 'application/json'})['data']\n        metadata = data['channel']\n\n        if metadata.get('online') == 0:\n            raise ExtractorError('Stream is offline', expected=True)\n\n        cdn_data = self._download_json(''.join((\n            update_url(data['getLoadBalancerUrl']['url'], scheme='https'),\n            '/stream/json_', metadata['stream_name'], '.js')),\n            channel_id, 'Downloading load balancing info')\n\n        formats = []\n        for source in (cdn_data.get('source') or []):\n            source_url = source.get('url')\n            if not source_url:\n                continue\n            source_type = source.get('type')\n            if source_type == 'html5/application/vnd.apple.mpegurl':\n                formats.extend(self._extract_m3u8_formats(\n                    source_url, channel_id, 'mp4', m3u8_id='hls', fatal=False))\n            elif source_type == 'html5/video/mp4':\n                formats.append({\n                    'url': source_url,","sourceCodeStart":35,"sourceCodeEnd":71,"githubUrl":"https://github.com/yt-dlp/yt-dlp/blob/81ecd58b1394793e6da9998cc19fdb45657f1685/yt_dlp/extractor/picarto.py#L35-L71","documentation":"PicartoIE (yt_dlp/extractor/picarto.py:53) extracts LIVE streams from picarto.tv channels. It queries the Picarto GraphQL API at https://ptvintern.picarto.tv/ptvapi for channel metadata, and if the returned channel.online field equals 0 the channel is not broadcasting, so extraction stops immediately with this expected error. There is no live feed to download in this state; this code path never returns formats for an offline channel.","triggerScenarios":"Running yt-dlp on a https://picarto.tv/<channel> URL (matched by PicartoIE, which defers to PicartoVodIE for /videos/ and /videopopout/ URLs) at a moment when the GraphQL response has channel.online == 0, i.e. the streamer is offline. Automated channel watchers typically hit this constantly.","commonSituations":"Monitoring scripts polling channels that are rarely live; following a scheduled-stream announcement page before the stream starts; a channel that was renamed so the slug points at a dead (offline) channel.","solutions":["Open https://picarto.tv/<channel> in a browser and confirm the channel is actually live before downloading.","If you want past broadcasts, use a VOD URL handled by PicartoVodIE (e.g. https://picarto.tv/<channel>/videos/<id> or https://picarto.tv/videopopout/<file>) instead of the live channel URL.","If you automate, poll the same GraphQL field (channel(name: ...) { online }) yourself and only invoke yt-dlp when it is 1.","Note yt-dlp's own test for this extractor is marked skip: 'Stream is offline' — hitting it is normal behaviour, not a bug."],"exampleFix":"# before: live-channel extraction while the streamer is offline\nyt-dlp https://picarto.tv/SomeChannel\n# after: fetch a recording via the VOD extractor instead\nyt-dlp https://picarto.tv/SomeChannel/videos/771008","handlingStrategy":"validation","validationCode":"import requests\n\ndef picarto_is_live(channel_id):\n    query = '{ channel(name: \"%s\") { online } }' % channel_id\n    res = requests.get(\n        'https://ptvintern.picarto.tv/ptvapi',\n        params={'query': query},\n        headers={'Accept': '*/*', 'Content-Type': 'application/json'},\n        timeout=10).json()\n    return res['data']['channel']['online'] == 1\n\n# only hand the URL to yt-dlp when the channel is actually broadcasting\nif picarto_is_live('SomeChannel'):\n    ...  # run YoutubeDL here","typeGuard":"from yt_dlp.utils import ExtractorError\n\ndef is_stream_offline(e: BaseException) -> bool:\n    \"\"\"True when e is the expected Picarto 'Stream is offline' failure.\"\"\"\n    return isinstance(e, ExtractorError) and e.expected and 'Stream is offline' in str(e)","tryCatchPattern":"from yt_dlp.utils import ExtractorError\n\ntry:\n    info = ydl.extract_info('https://picarto.tv/SomeChannel', download=True)\nexcept ExtractorError as e:\n    if e.expected and 'Stream is offline' in str(e):\n        logger.info('channel not broadcasting; skipping')\n    else:\n        raise","preventionTips":["Poll channel.online via the same GraphQL query the extractor uses before invoking yt-dlp.","For archives, always use /videos/<id> or /videopopout/ URLs (PicartoVodIE) instead of the live channel URL.","In watcher scripts, treat this expected error as a normal state, not an alert-worthy failure."],"tags":["picarto","live-stream","stream-offline","extractor","yt-dlp"],"backgroundTag":"stream-offline","analyzedSha":"81ecd58b1394793e6da9998cc19fdb45657f1685","analyzedAt":"2026-08-22T12:21:25.439Z","schemaVersion":2},"datasetVersion":"2026-08-22T14:17:55.899Z"}