youtube-dl

Another place where youtube-dl lives on
git clone git://git.oshgnacknak.de/youtube-dl.git
Log | Files | Refs | README | LICENSE

lbry.py (11266B)


      1 # coding: utf-8
      2 from __future__ import unicode_literals
      3 
      4 import functools
      5 import json
      6 
      7 from .common import InfoExtractor
      8 from ..compat import (
      9     compat_parse_qs,
     10     compat_str,
     11     compat_urllib_parse_unquote,
     12     compat_urllib_parse_urlparse,
     13 )
     14 from ..utils import (
     15     determine_ext,
     16     ExtractorError,
     17     int_or_none,
     18     mimetype2ext,
     19     OnDemandPagedList,
     20     try_get,
     21     urljoin,
     22 )
     23 
     24 
     25 class LBRYBaseIE(InfoExtractor):
     26     _BASE_URL_REGEX = r'https?://(?:www\.)?(?:lbry\.tv|odysee\.com)/'
     27     _CLAIM_ID_REGEX = r'[0-9a-f]{1,40}'
     28     _OPT_CLAIM_ID = '[^:/?#&]+(?::%s)?' % _CLAIM_ID_REGEX
     29     _SUPPORTED_STREAM_TYPES = ['video', 'audio']
     30 
     31     def _call_api_proxy(self, method, display_id, params, resource):
     32         return self._download_json(
     33             'https://api.lbry.tv/api/v1/proxy',
     34             display_id, 'Downloading %s JSON metadata' % resource,
     35             headers={'Content-Type': 'application/json-rpc'},
     36             data=json.dumps({
     37                 'method': method,
     38                 'params': params,
     39             }).encode())['result']
     40 
     41     def _resolve_url(self, url, display_id, resource):
     42         return self._call_api_proxy(
     43             'resolve', display_id, {'urls': url}, resource)[url]
     44 
     45     def _permanent_url(self, url, claim_name, claim_id):
     46         return urljoin(url, '/%s:%s' % (claim_name, claim_id))
     47 
     48     def _parse_stream(self, stream, url):
     49         stream_value = stream.get('value') or {}
     50         stream_type = stream_value.get('stream_type')
     51         source = stream_value.get('source') or {}
     52         media = stream_value.get(stream_type) or {}
     53         signing_channel = stream.get('signing_channel') or {}
     54         channel_name = signing_channel.get('name')
     55         channel_claim_id = signing_channel.get('claim_id')
     56         channel_url = None
     57         if channel_name and channel_claim_id:
     58             channel_url = self._permanent_url(url, channel_name, channel_claim_id)
     59 
     60         info = {
     61             'thumbnail': try_get(stream_value, lambda x: x['thumbnail']['url'], compat_str),
     62             'description': stream_value.get('description'),
     63             'license': stream_value.get('license'),
     64             'timestamp': int_or_none(stream.get('timestamp')),
     65             'release_timestamp': int_or_none(stream_value.get('release_time')),
     66             'tags': stream_value.get('tags'),
     67             'duration': int_or_none(media.get('duration')),
     68             'channel': try_get(signing_channel, lambda x: x['value']['title']),
     69             'channel_id': channel_claim_id,
     70             'channel_url': channel_url,
     71             'ext': determine_ext(source.get('name')) or mimetype2ext(source.get('media_type')),
     72             'filesize': int_or_none(source.get('size')),
     73         }
     74         if stream_type == 'audio':
     75             info['vcodec'] = 'none'
     76         else:
     77             info.update({
     78                 'width': int_or_none(media.get('width')),
     79                 'height': int_or_none(media.get('height')),
     80             })
     81         return info
     82 
     83 
     84 class LBRYIE(LBRYBaseIE):
     85     IE_NAME = 'lbry'
     86     _VALID_URL = LBRYBaseIE._BASE_URL_REGEX + r'(?P<id>\$/[^/]+/[^/]+/{1}|@{0}/{0}|(?!@){0})'.format(LBRYBaseIE._OPT_CLAIM_ID, LBRYBaseIE._CLAIM_ID_REGEX)
     87     _TESTS = [{
     88         # Video
     89         'url': 'https://lbry.tv/@Mantega:1/First-day-LBRY:1',
     90         'md5': '65bd7ec1f6744ada55da8e4c48a2edf9',
     91         'info_dict': {
     92             'id': '17f983b61f53091fb8ea58a9c56804e4ff8cff4d',
     93             'ext': 'mp4',
     94             'title': 'First day in LBRY? Start HERE!',
     95             'description': 'md5:f6cb5c704b332d37f5119313c2c98f51',
     96             'timestamp': 1595694354,
     97             'upload_date': '20200725',
     98             'release_timestamp': 1595340697,
     99             'release_date': '20200721',
    100             'width': 1280,
    101             'height': 720,
    102         }
    103     }, {
    104         # Audio
    105         'url': 'https://lbry.tv/@LBRYFoundation:0/Episode-1:e',
    106         'md5': 'c94017d3eba9b49ce085a8fad6b98d00',
    107         'info_dict': {
    108             'id': 'e7d93d772bd87e2b62d5ab993c1c3ced86ebb396',
    109             'ext': 'mp3',
    110             'title': 'The LBRY Foundation Community Podcast Episode 1 - Introduction, Streaming on LBRY, Transcoding',
    111             'description': 'md5:661ac4f1db09f31728931d7b88807a61',
    112             'timestamp': 1591312601,
    113             'upload_date': '20200604',
    114             'release_timestamp': 1591312421,
    115             'release_date': '20200604',
    116             'tags': list,
    117             'duration': 2570,
    118             'channel': 'The LBRY Foundation',
    119             'channel_id': '0ed629d2b9c601300cacf7eabe9da0be79010212',
    120             'channel_url': 'https://lbry.tv/@LBRYFoundation:0ed629d2b9c601300cacf7eabe9da0be79010212',
    121             'vcodec': 'none',
    122         }
    123     }, {
    124         # HLS
    125         'url': 'https://odysee.com/@gardeningincanada:b/plants-i-will-never-grow-again.-the:e',
    126         'md5': 'fc82f45ea54915b1495dd7cb5cc1289f',
    127         'info_dict': {
    128             'id': 'e51671357333fe22ae88aad320bde2f6f96b1410',
    129             'ext': 'mp4',
    130             'title': 'PLANTS I WILL NEVER GROW AGAIN. THE BLACK LIST PLANTS FOR A CANADIAN GARDEN | Gardening in Canada 🍁',
    131             'description': 'md5:9c539c6a03fb843956de61a4d5288d5e',
    132             'timestamp': 1618254123,
    133             'upload_date': '20210412',
    134             'release_timestamp': 1618254002,
    135             'release_date': '20210412',
    136             'tags': list,
    137             'duration': 554,
    138             'channel': 'Gardening In Canada',
    139             'channel_id': 'b8be0e93b423dad221abe29545fbe8ec36e806bc',
    140             'channel_url': 'https://odysee.com/@gardeningincanada:b8be0e93b423dad221abe29545fbe8ec36e806bc',
    141             'formats': 'mincount:3',
    142         }
    143     }, {
    144         'url': 'https://odysee.com/@BrodieRobertson:5/apple-is-tracking-everything-you-do-on:e',
    145         'only_matching': True,
    146     }, {
    147         'url': "https://odysee.com/@ScammerRevolts:b0/I-SYSKEY'D-THE-SAME-SCAMMERS-3-TIMES!:b",
    148         'only_matching': True,
    149     }, {
    150         'url': 'https://lbry.tv/Episode-1:e7d93d772bd87e2b62d5ab993c1c3ced86ebb396',
    151         'only_matching': True,
    152     }, {
    153         'url': 'https://lbry.tv/$/embed/Episode-1/e7d93d772bd87e2b62d5ab993c1c3ced86ebb396',
    154         'only_matching': True,
    155     }, {
    156         'url': 'https://lbry.tv/Episode-1:e7',
    157         'only_matching': True,
    158     }, {
    159         'url': 'https://lbry.tv/@LBRYFoundation/Episode-1',
    160         'only_matching': True,
    161     }, {
    162         'url': 'https://lbry.tv/$/download/Episode-1/e7d93d772bd87e2b62d5ab993c1c3ced86ebb396',
    163         'only_matching': True,
    164     }, {
    165         'url': 'https://lbry.tv/@lacajadepandora:a/TRUMP-EST%C3%81-BIEN-PUESTO-con-Pilar-Baselga,-Carlos-Senra,-Luis-Palacios-(720p_30fps_H264-192kbit_AAC):1',
    166         'only_matching': True,
    167     }]
    168 
    169     def _real_extract(self, url):
    170         display_id = self._match_id(url)
    171         if display_id.startswith('$/'):
    172             display_id = display_id.split('/', 2)[-1].replace('/', ':')
    173         else:
    174             display_id = display_id.replace(':', '#')
    175         display_id = compat_urllib_parse_unquote(display_id)
    176         uri = 'lbry://' + display_id
    177         result = self._resolve_url(uri, display_id, 'stream')
    178         result_value = result['value']
    179         if result_value.get('stream_type') not in self._SUPPORTED_STREAM_TYPES:
    180             raise ExtractorError('Unsupported URL', expected=True)
    181         claim_id = result['claim_id']
    182         title = result_value['title']
    183         streaming_url = self._call_api_proxy(
    184             'get', claim_id, {'uri': uri}, 'streaming url')['streaming_url']
    185         info = self._parse_stream(result, url)
    186         urlh = self._request_webpage(
    187             streaming_url, display_id, note='Downloading streaming redirect url info')
    188         if determine_ext(urlh.geturl()) == 'm3u8':
    189             info['formats'] = self._extract_m3u8_formats(
    190                 urlh.geturl(), display_id, 'mp4', entry_protocol='m3u8_native',
    191                 m3u8_id='hls')
    192             self._sort_formats(info['formats'])
    193         else:
    194             info['url'] = streaming_url
    195         info.update({
    196             'id': claim_id,
    197             'title': title,
    198         })
    199         return info
    200 
    201 
    202 class LBRYChannelIE(LBRYBaseIE):
    203     IE_NAME = 'lbry:channel'
    204     _VALID_URL = LBRYBaseIE._BASE_URL_REGEX + r'(?P<id>@%s)/?(?:[?#&]|$)' % LBRYBaseIE._OPT_CLAIM_ID
    205     _TESTS = [{
    206         'url': 'https://lbry.tv/@LBRYFoundation:0',
    207         'info_dict': {
    208             'id': '0ed629d2b9c601300cacf7eabe9da0be79010212',
    209             'title': 'The LBRY Foundation',
    210             'description': 'Channel for the LBRY Foundation. Follow for updates and news.',
    211         },
    212         'playlist_count': 29,
    213     }, {
    214         'url': 'https://lbry.tv/@LBRYFoundation',
    215         'only_matching': True,
    216     }]
    217     _PAGE_SIZE = 50
    218 
    219     def _fetch_page(self, claim_id, url, params, page):
    220         page += 1
    221         page_params = {
    222             'channel_ids': [claim_id],
    223             'claim_type': 'stream',
    224             'no_totals': True,
    225             'page': page,
    226             'page_size': self._PAGE_SIZE,
    227         }
    228         page_params.update(params)
    229         result = self._call_api_proxy(
    230             'claim_search', claim_id, page_params, 'page %d' % page)
    231         for item in (result.get('items') or []):
    232             stream_claim_name = item.get('name')
    233             stream_claim_id = item.get('claim_id')
    234             if not (stream_claim_name and stream_claim_id):
    235                 continue
    236 
    237             info = self._parse_stream(item, url)
    238             info.update({
    239                 '_type': 'url',
    240                 'id': stream_claim_id,
    241                 'title': try_get(item, lambda x: x['value']['title']),
    242                 'url': self._permanent_url(url, stream_claim_name, stream_claim_id),
    243             })
    244             yield info
    245 
    246     def _real_extract(self, url):
    247         display_id = self._match_id(url).replace(':', '#')
    248         result = self._resolve_url(
    249             'lbry://' + display_id, display_id, 'channel')
    250         claim_id = result['claim_id']
    251         qs = compat_parse_qs(compat_urllib_parse_urlparse(url).query)
    252         content = qs.get('content', [None])[0]
    253         params = {
    254             'fee_amount': qs.get('fee_amount', ['>=0'])[0],
    255             'order_by': {
    256                 'new': ['release_time'],
    257                 'top': ['effective_amount'],
    258                 'trending': ['trending_group', 'trending_mixed'],
    259             }[qs.get('order', ['new'])[0]],
    260             'stream_types': [content] if content in ['audio', 'video'] else self._SUPPORTED_STREAM_TYPES,
    261         }
    262         duration = qs.get('duration', [None])[0]
    263         if duration:
    264             params['duration'] = {
    265                 'long': '>=1200',
    266                 'short': '<=240',
    267             }[duration]
    268         language = qs.get('language', ['all'])[0]
    269         if language != 'all':
    270             languages = [language]
    271             if language == 'en':
    272                 languages.append('none')
    273             params['any_languages'] = languages
    274         entries = OnDemandPagedList(
    275             functools.partial(self._fetch_page, claim_id, url, params),
    276             self._PAGE_SIZE)
    277         result_value = result.get('value') or {}
    278         return self.playlist_result(
    279             entries, claim_id, result_value.get('title'),
    280             result_value.get('description'))