Repository: yt-dlp/yt-dlp
File: yt_dlp/extractor/twitch.py (Python)

Code before:
        is_live = None
        if thumbnail:
            if thumbnail.endswith('/404_processing_{width}x{height}.png'):
                is_live, thumbnail = True, None
            else:
                is_live = False
                for p in ('width', 'height'):
                    thumbnail = thumbnail.replace('{%s}' % p, '0')

        return {
            'id': vod_id,
            'title': info.get('title') or 'Untitled Broadcast',
            'description': info.get('description'),
            'duration': int_or_none(info.get('lengthSeconds')),
            'thumbnail': thumbnail,
            'uploader': try_get(info, lambda x: x['owner']['displayName'], compat_str),
            'uploader_id': try_get(info, lambda x: x['owner']['login'], compat_str),
            'timestamp': unified_timestamp(info.get('publishedAt')),
            'view_count': int_or_none(info.get('viewCount')),
            'chapters': list(self._extract_moments(info, item_id)),
            'is_live': is_live,
            'was_live': True,
        }

    def _extract_storyboard(self, item_id, storyboard_json_url, duration):
        spec = list(self._download_json(storyboard_json_url, item_id, "Downloading storyboard metadata JSON") or [])
        # sort from highest quality to lowest
        spec.sort(key=lambda x: int_or_none(x.get('width')) or 0, reverse=True)
        base = base_url(storyboard_json_url)
        for i, s in enumerate(spec):
            count = int_or_none(s.get('count'))
            images = list(s.get('images') or [])
            if len(images) == 0 or count is None:
                continue
            fragment_duration = duration / len(images)
            yield {
                'format_id': f'sb{i}',
                'format_note': 'storyboard',
                'ext': 'mhtml',
                'protocol': 'mhtml',
                'acodec': 'none',
                'vcodec': 'none',
                'url': urljoin(base, images[0]),
                'width': int_or_none(s.get('width')),
                'height': int_or_none(s.get('height')),
                'fps': count / duration,
                'rows': int_or_none(s.get('rows')),
                'columns': int_or_none(s.get('cols')),
                'fragments': [{
                    'url': urljoin(base, path),
                    'duration': fragment_duration,

Review comment (suggestion):
1. why cast to list?
2. Use single quotes
3. Must be non-fatal
```suggestion
        spec = self._download_json(storyboard_json_url, item_id, 'Downloading storyboard metadata JSON', fatal=False) or []
```

Code after:
        is_live = None
        if thumbnail:
            if thumbnail.endswith('/404_processing_{width}x{height}.png'):
                is_live, thumbnail = True, None
            else:
                is_live = False
                for p in ('width', 'height'):
                    thumbnail = thumbnail.replace('{%s}' % p, '0')

        return {
            'id': vod_id,
            'title': info.get('title') or 'Untitled Broadcast',
            'description': info.get('description'),
            'duration': int_or_none(info.get('lengthSeconds')),
            'thumbnail': thumbnail,
            'uploader': try_get(info, lambda x: x['owner']['displayName'], compat_str),
            'uploader_id': try_get(info, lambda x: x['owner']['login'], compat_str),
            'timestamp': unified_timestamp(info.get('publishedAt')),
            'view_count': int_or_none(info.get('viewCount')),
            'chapters': list(self._extract_moments(info, item_id)),
            'is_live': is_live,
            'was_live': True,
        }

    def _extract_storyboard(self, item_id, storyboard_json_url, duration):
        if not duration or not storyboard_json_url:
            return
        spec = self._download_json(storyboard_json_url, item_id, 'Downloading storyboard metadata JSON', fatal=False) or []
        # sort from highest quality to lowest
        # This makes sb0 the highest-quality format, sb1 - lower, etc which is consistent with youtube sb ordering
        spec.sort(key=lambda x: int_or_none(x.get('width')) or 0, reverse=True)
        base = base_url(storyboard_json_url)
        for i, s in enumerate(spec):
            count = int_or_none(s.get('count'))
            images = s.get('images')
            if not (images and count):
                continue
            fragment_duration = duration / len(images)
            yield {
                'format_id': f'sb{i}',
                'format_note': 'storyboard',
                'ext': 'mhtml',
                'protocol': 'mhtml',
                'acodec': 'none',
                'vcodec': 'none',
                'url': urljoin(base, images[0]),
                'width': int_or_none(s.get('width')),
                'height': int_or_none(s.get('height')),
                'fps': count / duration,
                'rows': int_or_none(s.get('rows')),
                'columns': int_or_none(s.get('cols')),