bitchute.py 12 KB

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