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

Code before:
class AsobiChannelTagURLIE(AsobiChannelBaseIE):
    IE_NAME = 'asobichannel:tag'
    IE_DESC = 'ASOBI CHANNEL'

    _VALID_URL = r'https?://asobichannel\.asobistore\.jp/tag/(?P<id>[a-z0-9-_]+)'
    _TESTS = [{
        'url': 'https://asobichannel.asobistore.jp/tag/bjhh-nbcja',
        'info_dict': {
            'id': 'bjhh-nbcja',
            'title': 'アイドルマスター ミリオンライブ！ 765プロch 原っぱ通信',
        },
        'playlist_mincount': 16,
    }, {
        'url': 'https://asobichannel.asobistore.jp/tag/hvm5qw3c6od',
        'info_dict': {
            'id': 'hvm5qw3c6od',
            'title': 'アイマスMOIW2023ラジオ',
        },
        'playlist_mincount': 13,
    }]

    def _real_extract(self, url):
        tag_id = self._match_id(url)

        webpage = self._download_webpage(url, tag_id)
        webpage_data = self._search_nextjs_data(webpage, tag_id)['props']['pageProps']

        media_list = self._download_json(
            f'https://channel.microcms.io/api/v1/media?limit=999&filters=(tag[contains]{tag_id})', tag_id,
            headers=self._MICROCMS_HEADER)

        entries = [{
            '_type': 'url',
            'url': f'https://asobichannel.asobistore.jp/watch/{metadata['id']}',
            **self._extract_info(metadata),
        } for metadata in media_list.get('contents', [])]

        return self.playlist_result(entries, tag_id, traverse_obj(webpage_data, ('data', 'name')))


Review comment (suggestion):
Since this is only used for the title, I would move it down at make it non fatal. 
```python
title = traverse_obj(self._search_nextjs_data(
    webpage, tag_id, fatal=False), ('props', 'pageProps', 'data', 'name'))
```
`fatal=False` does not currently work as expected for `_search_nextjs_data`, but that should be dealt with separately imo.

Code after:
            return merge_dicts(info, self._process_live(video_id, metadata))

        raise ExtractorError(f'Unexpected video type {video_type!r}')


class AsobiChannelTagURLIE(AsobiChannelBaseIE):
    IE_NAME = 'asobichannel:tag'
    IE_DESC = 'ASOBI CHANNEL'

    _VALID_URL = r'https?://asobichannel\.asobistore\.jp/tag/(?P<id>[a-z0-9-_]+)'
    _TESTS = [{
        'url': 'https://asobichannel.asobistore.jp/tag/bjhh-nbcja',
        'info_dict': {
            'id': 'bjhh-nbcja',
            'title': 'アイドルマスター ミリオンライブ！ 765プロch 原っぱ通信',
        },
        'playlist_mincount': 16,
    }, {
        'url': 'https://asobichannel.asobistore.jp/tag/hvm5qw3c6od',
        'info_dict': {
            'id': 'hvm5qw3c6od',
            'title': 'アイマスMOIW2023ラジオ',
        },
        'playlist_mincount': 13,
    }]

    def _real_extract(self, url):
        tag_id = self._match_id(url)
        webpage = self._download_webpage(url, tag_id)
        title = traverse_obj(self._search_nextjs_data(
            webpage, tag_id, fatal=False), ('props', 'pageProps', 'data', 'name', {str}))

        media = self._download_json(
            f'https://channel.microcms.io/api/v1/media?limit=999&filters=(tag[contains]{tag_id})',
            tag_id, headers=self._MICROCMS_HEADER)

        def entries():
            for metadata in traverse_obj(media, ('contents', lambda _, v: v['id'])):
                yield {
                    '_type': 'url',
                    'url': f'https://asobichannel.asobistore.jp/watch/{metadata["id"]}',
                    'ie_key': AsobiChannelIE.ie_key(),
                    **self._extract_info(metadata),
                }

        return self.playlist_result(entries(), tag_id, title)