minds.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. from .common import InfoExtractor
  2. from ..utils import (
  3. clean_html,
  4. format_field,
  5. int_or_none,
  6. str_or_none,
  7. strip_or_none,
  8. )
  9. class MindsBaseIE(InfoExtractor):
  10. _VALID_URL_BASE = r'https?://(?:www\.)?minds\.com/'
  11. def _call_api(self, path, video_id, resource, query=None):
  12. api_url = 'https://www.minds.com/api/' + path
  13. token = self._get_cookies(api_url).get('XSRF-TOKEN')
  14. return self._download_json(
  15. api_url, video_id, f'Downloading {resource} JSON metadata', headers={
  16. 'Referer': 'https://www.minds.com/',
  17. 'X-XSRF-TOKEN': token.value if token else '',
  18. }, query=query)
  19. class MindsIE(MindsBaseIE):
  20. IE_NAME = 'minds'
  21. _VALID_URL = MindsBaseIE._VALID_URL_BASE + r'(?:media|newsfeed|archive/view)/(?P<id>[0-9]+)'
  22. _TESTS = [{
  23. 'url': 'https://www.minds.com/media/100000000000086822',
  24. 'md5': '215a658184a419764852239d4970b045',
  25. 'info_dict': {
  26. 'id': '100000000000086822',
  27. 'ext': 'mp4',
  28. 'title': 'Minds intro sequence',
  29. 'thumbnail': r're:https?://.+\.png',
  30. 'uploader_id': 'ottman',
  31. 'upload_date': '20130524',
  32. 'timestamp': 1369404826,
  33. 'uploader': 'Bill Ottman',
  34. 'view_count': int,
  35. 'like_count': int,
  36. 'dislike_count': int,
  37. 'tags': ['animation'],
  38. 'comment_count': int,
  39. 'license': 'attribution-cc',
  40. },
  41. }, {
  42. # entity.type == 'activity' and empty title
  43. 'url': 'https://www.minds.com/newsfeed/798025111988506624',
  44. 'md5': 'b2733a74af78d7fd3f541c4cbbaa5950',
  45. 'info_dict': {
  46. 'id': '798022190320226304',
  47. 'ext': 'mp4',
  48. 'title': '798022190320226304',
  49. 'uploader': 'ColinFlaherty',
  50. 'upload_date': '20180111',
  51. 'timestamp': 1515639316,
  52. 'uploader_id': 'ColinFlaherty',
  53. },
  54. }, {
  55. 'url': 'https://www.minds.com/archive/view/715172106794442752',
  56. 'only_matching': True,
  57. }, {
  58. # youtube perma_url
  59. 'url': 'https://www.minds.com/newsfeed/1197131838022602752',
  60. 'only_matching': True,
  61. }]
  62. def _real_extract(self, url):
  63. entity_id = self._match_id(url)
  64. entity = self._call_api(
  65. 'v1/entities/entity/' + entity_id, entity_id, 'entity')['entity']
  66. if entity.get('type') == 'activity':
  67. if entity.get('custom_type') == 'video':
  68. video_id = entity['entity_guid']
  69. else:
  70. return self.url_result(entity['perma_url'])
  71. else:
  72. assert entity['subtype'] == 'video'
  73. video_id = entity_id
  74. # 1080p and webm formats available only on the sources array
  75. video = self._call_api(
  76. 'v2/media/video/' + video_id, video_id, 'video')
  77. formats = []
  78. for source in (video.get('sources') or []):
  79. src = source.get('src')
  80. if not src:
  81. continue
  82. formats.append({
  83. 'format_id': source.get('label'),
  84. 'height': int_or_none(source.get('size')),
  85. 'url': src,
  86. })
  87. entity = video.get('entity') or entity
  88. owner = entity.get('ownerObj') or {}
  89. uploader_id = owner.get('username')
  90. tags = entity.get('tags')
  91. if tags and isinstance(tags, str):
  92. tags = [tags]
  93. thumbnail = None
  94. poster = video.get('poster') or entity.get('thumbnail_src')
  95. if poster:
  96. urlh = self._request_webpage(poster, video_id, fatal=False)
  97. if urlh:
  98. thumbnail = urlh.url
  99. return {
  100. 'id': video_id,
  101. 'title': entity.get('title') or video_id,
  102. 'formats': formats,
  103. 'description': clean_html(entity.get('description')) or None,
  104. 'license': str_or_none(entity.get('license')),
  105. 'timestamp': int_or_none(entity.get('time_created')),
  106. 'uploader': strip_or_none(owner.get('name')),
  107. 'uploader_id': uploader_id,
  108. 'uploader_url': format_field(uploader_id, None, 'https://www.minds.com/%s'),
  109. 'view_count': int_or_none(entity.get('play:count')),
  110. 'like_count': int_or_none(entity.get('thumbs:up:count')),
  111. 'dislike_count': int_or_none(entity.get('thumbs:down:count')),
  112. 'tags': tags,
  113. 'comment_count': int_or_none(entity.get('comments:count')),
  114. 'thumbnail': thumbnail,
  115. }
  116. class MindsFeedBaseIE(MindsBaseIE):
  117. _PAGE_SIZE = 150
  118. def _entries(self, feed_id):
  119. query = {'limit': self._PAGE_SIZE, 'sync': 1}
  120. i = 1
  121. while True:
  122. data = self._call_api(
  123. f'v2/feeds/container/{feed_id}/videos',
  124. feed_id, f'page {i}', query)
  125. entities = data.get('entities') or []
  126. for entity in entities:
  127. guid = entity.get('guid')
  128. if not guid:
  129. continue
  130. yield self.url_result(
  131. 'https://www.minds.com/newsfeed/' + guid,
  132. MindsIE.ie_key(), guid)
  133. query['from_timestamp'] = data['load-next']
  134. if not (query['from_timestamp'] and len(entities) == self._PAGE_SIZE):
  135. break
  136. i += 1
  137. def _real_extract(self, url):
  138. feed_id = self._match_id(url)
  139. feed = self._call_api(
  140. f'v1/{self._FEED_PATH}/{feed_id}',
  141. feed_id, self._FEED_TYPE)[self._FEED_TYPE]
  142. return self.playlist_result(
  143. self._entries(feed['guid']), feed_id,
  144. strip_or_none(feed.get('name')),
  145. feed.get('briefdescription'))
  146. class MindsChannelIE(MindsFeedBaseIE):
  147. _FEED_TYPE = 'channel'
  148. IE_NAME = 'minds:' + _FEED_TYPE
  149. _VALID_URL = MindsBaseIE._VALID_URL_BASE + r'(?!(?:newsfeed|media|api|archive|groups)/)(?P<id>[^/?&#]+)'
  150. _FEED_PATH = 'channel'
  151. _TEST = {
  152. 'url': 'https://www.minds.com/ottman',
  153. 'info_dict': {
  154. 'id': 'ottman',
  155. 'title': 'Bill Ottman',
  156. 'description': 'Co-creator & CEO @minds',
  157. },
  158. 'playlist_mincount': 54,
  159. }
  160. class MindsGroupIE(MindsFeedBaseIE):
  161. _FEED_TYPE = 'group'
  162. IE_NAME = 'minds:' + _FEED_TYPE
  163. _VALID_URL = MindsBaseIE._VALID_URL_BASE + r'groups/profile/(?P<id>[0-9]+)'
  164. _FEED_PATH = 'groups/group'
  165. _TEST = {
  166. 'url': 'https://www.minds.com/groups/profile/785582576369672204/feed/videos',
  167. 'info_dict': {
  168. 'id': '785582576369672204',
  169. 'title': 'Cooking Videos',
  170. },
  171. 'playlist_mincount': 1,
  172. }