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

Code before:
                f'{self.IE_NAME} said: {err.get("code")} - {err.get("message")}', expected=True)
        return response['result']

    def _resolve_url(self, url, display_id, resource):
        return self._call_api_proxy(
            'resolve', display_id, {'urls': url}, resource)[url]

    def _permanent_url(self, url, claim_name, claim_id):
        return urljoin(
            url.replace('lbry://', 'https://lbry.tv/'),
            '/%s:%s' % (claim_name, claim_id))

    def _parse_stream(self, stream, url):
        stream_type = traverse_obj(stream, ('value', 'stream_type', {str}))

        info = traverse_obj(stream, {
            'title': ('value', 'title', {str}),
            'thumbnail': ('value', 'thumbnail', 'url', {url_or_none}),
            'description': ('value', 'description', {str}),
            'license': ('value', 'license', {str}),
            'timestamp': ('timestamp', {int_or_none}),
            'release_timestamp': ('value', 'release_time', {int_or_none}),
            'tags': ('value', 'tags', ..., {lambda x: x or None}),
            'duration': ('value', stream_type, 'duration', {int_or_none}),
            'channel_id': ('signing_channel', 'claim_id', {str}),
        })

        info['uploader_id'] = traverse_obj(stream, ('signing_channel', 'normalized_name', {str}))
        channel_name = traverse_obj(stream, ('signing_channel', (('value', 'title'), 'name'), {str}), get_all=False)
        info['channel'] = channel_name
        if channel_name and info.get('channel_id'):
            info['channel_url'] = self._permanent_url(url, channel_name, info['channel_id'])

        return info


class LBRYIE(LBRYBaseIE):
    IE_NAME = 'lbry'
    _VALID_URL = LBRYBaseIE._BASE_URL_REGEX + r'(?P<id>\$/[^/]+/[^/]+/{1}|@{0}/{0}|(?!@){0})'.format(LBRYBaseIE._OPT_CLAIM_ID, LBRYBaseIE._CLAIM_ID_REGEX)
    _TESTS = [{
        # Video
        'url': 'https://lbry.tv/@Mantega:1/First-day-LBRY:1',
        'md5': '65bd7ec1f6744ada55da8e4c48a2edf9',
        'info_dict': {
            'id': '17f983b61f53091fb8ea58a9c56804e4ff8cff4d',
            'ext': 'mp4',
            'title': 'First day in LBRY? Start HERE!',
            'description': 'md5:f6cb5c704b332d37f5119313c2c98f51',
            'timestamp': 1595694354,
            'upload_date': '20200725',
            'release_timestamp': 1595340697,

Review comment (suggestion):
Do not remove `channel` here; it makes no sense to do so.
If we want to add `uploader_id`, then let's do it here
```suggestion
            'channel': ('signing_channel', 'value', 'title', {str}),
            'channel_id': ('signing_channel', 'claim_id', {str}),
            'uploader_id': ('signing_channel', 'name', {str}),
        })
```

Code after:
            raise ExtractorError(
                f'{self.IE_NAME} said: {err.get("code")} - {err.get("message")}', expected=True)
        return response['result']

    def _resolve_url(self, url, display_id, resource):
        return self._call_api_proxy(
            'resolve', display_id, {'urls': url}, resource)[url]

    def _permanent_url(self, url, claim_name, claim_id):
        return urljoin(
            url.replace('lbry://', 'https://lbry.tv/'),
            '/%s:%s' % (claim_name, claim_id))

    def _parse_stream(self, stream, url):
        stream_type = traverse_obj(stream, ('value', 'stream_type', {str}))

        info = traverse_obj(stream, {
            'title': ('value', 'title', {str}),
            'thumbnail': ('value', 'thumbnail', 'url', {url_or_none}),
            'description': ('value', 'description', {str}),
            'license': ('value', 'license', {str}),
            'timestamp': ('timestamp', {int_or_none}),
            'release_timestamp': ('value', 'release_time', {int_or_none}),
            'tags': ('value', 'tags', ..., {lambda x: x or None}),
            'duration': ('value', stream_type, 'duration', {int_or_none}),
            'channel': ('signing_channel', 'value', 'title', {str}),
            'channel_id': ('signing_channel', 'claim_id', {str}),
            'uploader_id': ('signing_channel', 'name', {str}),
        })

        if info.get('uploader_id') and info.get('channel_id'):
            info['channel_url'] = self._permanent_url(url, info['uploader_id'], info['channel_id'])

        return info

    def _fetch_page(self, display_id, url, params, page):
        page += 1
        page_params = {
            'no_totals': True,
            'page': page,
            'page_size': self._PAGE_SIZE,
            **params,
        }
        result = self._call_api_proxy(
            'claim_search', display_id, page_params, f'page {page}')
        for item in traverse_obj(result, ('items', lambda _, v: v['name'] and v['claim_id'])):
            yield {
                **self._parse_stream(item, url),
                '_type': 'url',
                'id': item['claim_id'],
                'url': self._permanent_url(url, item['name'], item['claim_id']),