]> asedeno.scripts.mit.edu Git - youtube-dl.git/blob - youtube_dl/extractor/lbry.py
Return the item itself if playlist has one entry
[youtube-dl.git] / youtube_dl / extractor / lbry.py
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'))