murrtube.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. import functools
  2. import json
  3. from .common import InfoExtractor
  4. from ..utils import (
  5. ExtractorError,
  6. OnDemandPagedList,
  7. determine_ext,
  8. int_or_none,
  9. try_get,
  10. )
  11. class MurrtubeIE(InfoExtractor):
  12. _VALID_URL = r'''(?x)
  13. (?:
  14. murrtube:|
  15. https?://murrtube\.net/videos/(?P<slug>[a-z0-9\-]+)\-
  16. )
  17. (?P<id>[a-f0-9]{8}\-[a-f0-9]{4}\-[a-f0-9]{4}\-[a-f0-9]{4}\-[a-f0-9]{12})
  18. '''
  19. _TEST = {
  20. 'url': 'https://murrtube.net/videos/inferno-x-skyler-148b6f2a-fdcc-4902-affe-9c0f41aaaca0',
  21. 'md5': '169f494812d9a90914b42978e73aa690',
  22. 'info_dict': {
  23. 'id': '148b6f2a-fdcc-4902-affe-9c0f41aaaca0',
  24. 'ext': 'mp4',
  25. 'title': 'Inferno X Skyler',
  26. 'description': 'Humping a very good slutty sheppy (roomate)',
  27. 'thumbnail': r're:^https?://.*\.jpg$',
  28. 'duration': 284,
  29. 'uploader': 'Inferno Wolf',
  30. 'age_limit': 18,
  31. 'comment_count': int,
  32. 'view_count': int,
  33. 'like_count': int,
  34. 'tags': ['hump', 'breed', 'Fursuit', 'murrsuit', 'bareback'],
  35. }
  36. }
  37. def _download_gql(self, video_id, op, note=None, fatal=True):
  38. result = self._download_json(
  39. 'https://murrtube.net/graphql',
  40. video_id, note, data=json.dumps(op).encode(), fatal=fatal,
  41. headers={'Content-Type': 'application/json'})
  42. return result['data']
  43. def _real_extract(self, url):
  44. video_id = self._match_id(url)
  45. data = self._download_gql(video_id, {
  46. 'operationName': 'Medium',
  47. 'variables': {
  48. 'id': video_id,
  49. },
  50. 'query': '''\
  51. query Medium($id: ID!) {
  52. medium(id: $id) {
  53. title
  54. description
  55. key
  56. duration
  57. commentsCount
  58. likesCount
  59. viewsCount
  60. thumbnailKey
  61. tagList
  62. user {
  63. name
  64. __typename
  65. }
  66. __typename
  67. }
  68. }'''})
  69. meta = data['medium']
  70. storage_url = 'https://storage.murrtube.net/murrtube/'
  71. format_url = storage_url + meta.get('key', '')
  72. thumbnail = storage_url + meta.get('thumbnailKey', '')
  73. if determine_ext(format_url) == 'm3u8':
  74. formats = self._extract_m3u8_formats(
  75. format_url, video_id, 'mp4', entry_protocol='m3u8_native', fatal=False)
  76. else:
  77. formats = [{'url': format_url}]
  78. return {
  79. 'id': video_id,
  80. 'title': meta.get('title'),
  81. 'description': meta.get('description'),
  82. 'formats': formats,
  83. 'thumbnail': thumbnail,
  84. 'duration': int_or_none(meta.get('duration')),
  85. 'uploader': try_get(meta, lambda x: x['user']['name']),
  86. 'view_count': meta.get('viewsCount'),
  87. 'like_count': meta.get('likesCount'),
  88. 'comment_count': meta.get('commentsCount'),
  89. 'tags': meta.get('tagList'),
  90. 'age_limit': 18,
  91. }
  92. class MurrtubeUserIE(MurrtubeIE): # XXX: Do not subclass from concrete IE
  93. IE_DESC = 'Murrtube user profile'
  94. _VALID_URL = r'https?://murrtube\.net/(?P<id>[^/]+)$'
  95. _TEST = {
  96. 'url': 'https://murrtube.net/stormy',
  97. 'info_dict': {
  98. 'id': 'stormy',
  99. },
  100. 'playlist_mincount': 27,
  101. }
  102. _PAGE_SIZE = 10
  103. def _fetch_page(self, username, user_id, page):
  104. data = self._download_gql(username, {
  105. 'operationName': 'Media',
  106. 'variables': {
  107. 'limit': self._PAGE_SIZE,
  108. 'offset': page * self._PAGE_SIZE,
  109. 'sort': 'latest',
  110. 'userId': user_id,
  111. },
  112. 'query': '''\
  113. query Media($q: String, $sort: String, $userId: ID, $offset: Int!, $limit: Int!) {
  114. media(q: $q, sort: $sort, userId: $userId, offset: $offset, limit: $limit) {
  115. id
  116. __typename
  117. }
  118. }'''},
  119. 'Downloading page {0}'.format(page + 1))
  120. if data is None:
  121. raise ExtractorError(f'Failed to retrieve video list for page {page + 1}')
  122. media = data['media']
  123. for entry in media:
  124. yield self.url_result('murrtube:{0}'.format(entry['id']), MurrtubeIE.ie_key())
  125. def _real_extract(self, url):
  126. username = self._match_id(url)
  127. data = self._download_gql(username, {
  128. 'operationName': 'User',
  129. 'variables': {
  130. 'id': username,
  131. },
  132. 'query': '''\
  133. query User($id: ID!) {
  134. user(id: $id) {
  135. id
  136. __typename
  137. }
  138. }'''},
  139. 'Downloading user info')
  140. if data is None:
  141. raise ExtractorError('Failed to fetch user info')
  142. user = data['user']
  143. entries = OnDemandPagedList(functools.partial(
  144. self._fetch_page, username, user.get('id')), self._PAGE_SIZE)
  145. return self.playlist_result(entries, username)