bitchute.py 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. import functools
  2. import re
  3. from .common import InfoExtractor
  4. from ..utils import (
  5. ExtractorError,
  6. HEADRequest,
  7. OnDemandPagedList,
  8. clean_html,
  9. get_element_by_class,
  10. get_element_by_id,
  11. get_elements_html_by_class,
  12. int_or_none,
  13. orderedSet,
  14. parse_count,
  15. parse_duration,
  16. traverse_obj,
  17. unified_strdate,
  18. urlencode_postdata,
  19. )
  20. class BitChuteIE(InfoExtractor):
  21. _VALID_URL = r'https?://(?:www\.)?bitchute\.com/(?:video|embed|torrent/[^/]+)/(?P<id>[^/?#&]+)'
  22. _EMBED_REGEX = [rf'<(?:script|iframe)[^>]+\bsrc=(["\'])(?P<url>{_VALID_URL})']
  23. _TESTS = [{
  24. 'url': 'https://www.bitchute.com/video/UGlrF9o9b-Q/',
  25. 'md5': '7e427d7ed7af5a75b5855705ec750e2b',
  26. 'info_dict': {
  27. 'id': 'UGlrF9o9b-Q',
  28. 'ext': 'mp4',
  29. 'title': 'This is the first video on #BitChute !',
  30. 'description': 'md5:a0337e7b1fe39e32336974af8173a034',
  31. 'thumbnail': r're:^https?://.*\.jpg$',
  32. 'uploader': 'BitChute',
  33. 'upload_date': '20170103',
  34. },
  35. }, {
  36. # video not downloadable in browser, but we can recover it
  37. 'url': 'https://www.bitchute.com/video/2s6B3nZjAk7R/',
  38. 'md5': '05c12397d5354bf24494885b08d24ed1',
  39. 'info_dict': {
  40. 'id': '2s6B3nZjAk7R',
  41. 'ext': 'mp4',
  42. 'filesize': 71537926,
  43. 'title': 'STYXHEXENHAMMER666 - Election Fraud, Clinton 2020, EU Armies, and Gun Control',
  44. 'description': 'md5:228ee93bd840a24938f536aeac9cf749',
  45. 'thumbnail': r're:^https?://.*\.jpg$',
  46. 'uploader': 'BitChute',
  47. 'upload_date': '20181113',
  48. },
  49. 'params': {'check_formats': None},
  50. }, {
  51. # restricted video
  52. 'url': 'https://www.bitchute.com/video/WEnQU7XGcTdl/',
  53. 'info_dict': {
  54. 'id': 'WEnQU7XGcTdl',
  55. 'ext': 'mp4',
  56. 'title': 'Impartial Truth - Ein Letzter Appell an die Vernunft',
  57. },
  58. 'params': {'skip_download': True},
  59. 'skip': 'Georestricted in DE',
  60. }, {
  61. 'url': 'https://www.bitchute.com/embed/lbb5G1hjPhw/',
  62. 'only_matching': True,
  63. }, {
  64. 'url': 'https://www.bitchute.com/torrent/Zee5BE49045h/szoMrox2JEI.webtorrent',
  65. 'only_matching': True,
  66. }]
  67. _GEO_BYPASS = False
  68. _HEADERS = {
  69. 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.57 Safari/537.36',
  70. 'Referer': 'https://www.bitchute.com/',
  71. }
  72. def _check_format(self, video_url, video_id):
  73. urls = orderedSet(
  74. re.sub(r'(^https?://)(seed\d+)(?=\.bitchute\.com)', fr'\g<1>{host}', video_url)
  75. for host in (r'\g<2>', 'seed150', 'seed151', 'seed152', 'seed153'))
  76. for url in urls:
  77. try:
  78. response = self._request_webpage(
  79. HEADRequest(url), video_id=video_id, note=f'Checking {url}', headers=self._HEADERS)
  80. except ExtractorError as e:
  81. self.to_screen(f'{video_id}: URL is invalid, skipping: {e.cause}')
  82. continue
  83. return {
  84. 'url': url,
  85. 'filesize': int_or_none(response.headers.get('Content-Length'))
  86. }
  87. def _raise_if_restricted(self, webpage):
  88. page_title = clean_html(get_element_by_class('page-title', webpage)) or ''
  89. if re.fullmatch(r'(?:Channel|Video) Restricted', page_title):
  90. reason = clean_html(get_element_by_id('page-detail', webpage)) or page_title
  91. self.raise_geo_restricted(reason)
  92. def _real_extract(self, url):
  93. video_id = self._match_id(url)
  94. webpage = self._download_webpage(
  95. f'https://www.bitchute.com/video/{video_id}', video_id, headers=self._HEADERS)
  96. self._raise_if_restricted(webpage)
  97. publish_date = clean_html(get_element_by_class('video-publish-date', webpage))
  98. entries = self._parse_html5_media_entries(url, webpage, video_id)
  99. formats = []
  100. for format_ in traverse_obj(entries, (0, 'formats', ...)):
  101. if self.get_param('check_formats') is not False:
  102. format_.update(self._check_format(format_.pop('url'), video_id) or {})
  103. if 'url' not in format_:
  104. continue
  105. formats.append(format_)
  106. if not formats:
  107. self.raise_no_formats(
  108. 'Video is unavailable. Please make sure this video is playable in the browser '
  109. 'before reporting this issue.', expected=True, video_id=video_id)
  110. return {
  111. 'id': video_id,
  112. 'title': self._html_extract_title(webpage) or self._og_search_title(webpage),
  113. 'description': self._og_search_description(webpage, default=None),
  114. 'thumbnail': self._og_search_thumbnail(webpage),
  115. 'uploader': clean_html(get_element_by_class('owner', webpage)),
  116. 'upload_date': unified_strdate(self._search_regex(
  117. r'at \d+:\d+ UTC on (.+?)\.', publish_date, 'upload date', fatal=False)),
  118. 'formats': formats,
  119. }
  120. class BitChuteChannelIE(InfoExtractor):
  121. _VALID_URL = r'https?://(?:www\.)?bitchute\.com/(?P<type>channel|playlist)/(?P<id>[^/?#&]+)'
  122. _TESTS = [{
  123. 'url': 'https://www.bitchute.com/channel/bitchute/',
  124. 'info_dict': {
  125. 'id': 'bitchute',
  126. 'title': 'BitChute',
  127. 'description': 'md5:5329fb3866125afa9446835594a9b138',
  128. },
  129. 'playlist': [
  130. {
  131. 'md5': '7e427d7ed7af5a75b5855705ec750e2b',
  132. 'info_dict': {
  133. 'id': 'UGlrF9o9b-Q',
  134. 'ext': 'mp4',
  135. 'filesize': None,
  136. 'title': 'This is the first video on #BitChute !',
  137. 'description': 'md5:a0337e7b1fe39e32336974af8173a034',
  138. 'thumbnail': r're:^https?://.*\.jpg$',
  139. 'uploader': 'BitChute',
  140. 'upload_date': '20170103',
  141. 'duration': 16,
  142. 'view_count': int,
  143. },
  144. }
  145. ],
  146. 'params': {
  147. 'skip_download': True,
  148. 'playlist_items': '-1',
  149. },
  150. }, {
  151. 'url': 'https://www.bitchute.com/playlist/wV9Imujxasw9/',
  152. 'playlist_mincount': 20,
  153. 'info_dict': {
  154. 'id': 'wV9Imujxasw9',
  155. 'title': 'Bruce MacDonald and "The Light of Darkness"',
  156. 'description': 'md5:04913227d2714af1d36d804aa2ab6b1e',
  157. }
  158. }]
  159. _TOKEN = 'zyG6tQcGPE5swyAEFLqKUwMuMMuF6IO2DZ6ZDQjGfsL0e4dcTLwqkTTul05Jdve7'
  160. PAGE_SIZE = 25
  161. HTML_CLASS_NAMES = {
  162. 'channel': {
  163. 'container': 'channel-videos-container',
  164. 'title': 'channel-videos-title',
  165. 'description': 'channel-videos-text',
  166. },
  167. 'playlist': {
  168. 'container': 'playlist-video',
  169. 'title': 'title',
  170. 'description': 'description',
  171. }
  172. }
  173. @staticmethod
  174. def _make_url(playlist_id, playlist_type):
  175. return f'https://www.bitchute.com/{playlist_type}/{playlist_id}/'
  176. def _fetch_page(self, playlist_id, playlist_type, page_num):
  177. playlist_url = self._make_url(playlist_id, playlist_type)
  178. data = self._download_json(
  179. f'{playlist_url}extend/', playlist_id, f'Downloading page {page_num}',
  180. data=urlencode_postdata({
  181. 'csrfmiddlewaretoken': self._TOKEN,
  182. 'name': '',
  183. 'offset': page_num * self.PAGE_SIZE,
  184. }), headers={
  185. 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
  186. 'Referer': playlist_url,
  187. 'X-Requested-With': 'XMLHttpRequest',
  188. 'Cookie': f'csrftoken={self._TOKEN}',
  189. })
  190. if not data.get('success'):
  191. return
  192. classes = self.HTML_CLASS_NAMES[playlist_type]
  193. for video_html in get_elements_html_by_class(classes['container'], data.get('html')):
  194. video_id = self._search_regex(
  195. r'<a\s[^>]*\bhref=["\']/video/([^"\'/]+)', video_html, 'video id', default=None)
  196. if not video_id:
  197. continue
  198. yield self.url_result(
  199. f'https://www.bitchute.com/video/{video_id}', BitChuteIE, video_id, url_transparent=True,
  200. title=clean_html(get_element_by_class(classes['title'], video_html)),
  201. description=clean_html(get_element_by_class(classes['description'], video_html)),
  202. duration=parse_duration(get_element_by_class('video-duration', video_html)),
  203. view_count=parse_count(clean_html(get_element_by_class('video-views', video_html))))
  204. def _real_extract(self, url):
  205. playlist_type, playlist_id = self._match_valid_url(url).group('type', 'id')
  206. webpage = self._download_webpage(self._make_url(playlist_id, playlist_type), playlist_id)
  207. page_func = functools.partial(self._fetch_page, playlist_id, playlist_type)
  208. return self.playlist_result(
  209. OnDemandPagedList(page_func, self.PAGE_SIZE), playlist_id,
  210. title=self._html_extract_title(webpage, default=None),
  211. description=self._html_search_meta(
  212. ('description', 'og:description', 'twitter:description'), webpage, default=None),
  213. playlist_count=int_or_none(self._html_search_regex(
  214. r'<span>(\d+)\s+videos?</span>', webpage, 'playlist count', default=None)))