facebook.py 47 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058
  1. import json
  2. import re
  3. import urllib.parse
  4. from .common import InfoExtractor
  5. from ..compat import compat_etree_fromstring
  6. from ..networking import Request
  7. from ..networking.exceptions import network_exceptions
  8. from ..utils import (
  9. ExtractorError,
  10. clean_html,
  11. determine_ext,
  12. float_or_none,
  13. format_field,
  14. get_element_by_id,
  15. get_first,
  16. int_or_none,
  17. join_nonempty,
  18. js_to_json,
  19. merge_dicts,
  20. parse_count,
  21. parse_qs,
  22. qualities,
  23. str_or_none,
  24. traverse_obj,
  25. try_get,
  26. url_or_none,
  27. urlencode_postdata,
  28. urljoin,
  29. variadic,
  30. )
  31. class FacebookIE(InfoExtractor):
  32. _VALID_URL = r'''(?x)
  33. (?:
  34. https?://
  35. (?:[\w-]+\.)?(?:facebook\.com|facebookwkhpilnemxj7asaniu7vnjjbiltxjqhye3mhbshg7kx5tfyd\.onion)/
  36. (?:[^#]*?\#!/)?
  37. (?:
  38. (?:
  39. permalink\.php|
  40. video/video\.php|
  41. photo\.php|
  42. video\.php|
  43. video/embed|
  44. story\.php|
  45. watch(?:/live)?/?
  46. )\?(?:.*?)(?:v|video_id|story_fbid)=|
  47. [^/]+/videos/(?:[^/]+/)?|
  48. [^/]+/posts/|
  49. events/(?:[^/]+/)?|
  50. groups/[^/]+/(?:permalink|posts)/|
  51. watchparty/
  52. )|
  53. facebook:
  54. )
  55. (?P<id>pfbid[A-Za-z0-9]+|\d+)
  56. '''
  57. _EMBED_REGEX = [
  58. r'<iframe[^>]+?src=(["\'])(?P<url>https?://www\.facebook\.com/(?:video/embed|plugins/video\.php).+?)\1',
  59. # Facebook API embed https://developers.facebook.com/docs/plugins/embedded-video-player
  60. r'''(?x)<div[^>]+
  61. class=(?P<q1>[\'"])[^\'"]*\bfb-(?:video|post)\b[^\'"]*(?P=q1)[^>]+
  62. data-href=(?P<q2>[\'"])(?P<url>(?:https?:)?//(?:www\.)?facebook.com/.+?)(?P=q2)''',
  63. ]
  64. _LOGIN_URL = 'https://www.facebook.com/login.php?next=http%3A%2F%2Ffacebook.com%2Fhome.php&login_attempt=1'
  65. _CHECKPOINT_URL = 'https://www.facebook.com/checkpoint/?next=http%3A%2F%2Ffacebook.com%2Fhome.php&_fb_noscript=1'
  66. _NETRC_MACHINE = 'facebook'
  67. IE_NAME = 'facebook'
  68. _VIDEO_PAGE_TEMPLATE = 'https://www.facebook.com/video/video.php?v=%s'
  69. _VIDEO_PAGE_TAHOE_TEMPLATE = 'https://www.facebook.com/video/tahoe/async/%s/?chain=true&isvideo=true&payloadtype=primary'
  70. _TESTS = [{
  71. 'url': 'https://www.facebook.com/radiokicksfm/videos/3676516585958356/',
  72. 'info_dict': {
  73. 'id': '3676516585958356',
  74. 'ext': 'mp4',
  75. 'title': 'dr Adam Przygoda',
  76. 'description': 'md5:34675bda53336b1d16400265c2bb9b3b',
  77. 'uploader': 'RADIO KICKS FM',
  78. 'upload_date': '20230818',
  79. 'timestamp': 1692346159,
  80. 'thumbnail': r're:^https?://.*',
  81. 'uploader_id': '100063551323670',
  82. 'duration': 3132.184,
  83. 'view_count': int,
  84. 'concurrent_view_count': 0,
  85. },
  86. }, {
  87. 'url': 'https://www.facebook.com/video.php?v=637842556329505&fref=nf',
  88. 'md5': '6a40d33c0eccbb1af76cf0485a052659',
  89. 'info_dict': {
  90. 'id': '637842556329505',
  91. 'ext': 'mp4',
  92. 'title': 're:Did you know Kei Nishikori is the first Asian man to ever reach a Grand Slam',
  93. 'uploader': 'Tennis on Facebook',
  94. 'upload_date': '20140908',
  95. 'timestamp': 1410199200,
  96. },
  97. 'skip': 'Requires logging in',
  98. }, {
  99. # data.video
  100. 'url': 'https://www.facebook.com/video.php?v=274175099429670',
  101. 'info_dict': {
  102. 'id': '274175099429670',
  103. 'ext': 'mp4',
  104. 'title': 'Asif',
  105. 'description': '',
  106. 'uploader': 'Asif Nawab Butt',
  107. 'upload_date': '20140506',
  108. 'timestamp': 1399398998,
  109. 'thumbnail': r're:^https?://.*',
  110. 'uploader_id': 'pfbid028wxorhX2ErLFJ578N6P3crHD3PHmXTCqCvfBpsnbSLmbokwSY75p5hWBjHGkG4zxl',
  111. 'duration': 131.03,
  112. 'concurrent_view_count': int,
  113. },
  114. }, {
  115. 'note': 'Video with DASH manifest',
  116. 'url': 'https://www.facebook.com/video.php?v=957955867617029',
  117. 'md5': 'b2c28d528273b323abe5c6ab59f0f030',
  118. 'info_dict': {
  119. 'id': '957955867617029',
  120. 'ext': 'mp4',
  121. 'title': 'When you post epic content on instagram.com/433 8 million followers, this is ...',
  122. 'uploader': 'Demy de Zeeuw',
  123. 'upload_date': '20160110',
  124. 'timestamp': 1452431627,
  125. },
  126. 'skip': 'Requires logging in',
  127. }, {
  128. 'url': 'https://www.facebook.com/maxlayn/posts/10153807558977570',
  129. 'md5': '037b1fa7f3c2d02b7a0d7bc16031ecc6',
  130. 'info_dict': {
  131. 'id': '544765982287235',
  132. 'ext': 'mp4',
  133. 'title': '"What are you doing running in the snow?"',
  134. 'uploader': 'FailArmy',
  135. },
  136. 'skip': 'Video gone',
  137. }, {
  138. 'url': 'https://m.facebook.com/story.php?story_fbid=1035862816472149&id=116132035111903',
  139. 'md5': '1deb90b6ac27f7efcf6d747c8a27f5e3',
  140. 'info_dict': {
  141. 'id': '1035862816472149',
  142. 'ext': 'mp4',
  143. 'title': 'What the Flock Is Going On In New Zealand Credit: ViralHog',
  144. 'uploader': 'S. Saint',
  145. },
  146. 'skip': 'Video gone',
  147. }, {
  148. 'note': 'swf params escaped',
  149. 'url': 'https://www.facebook.com/barackobama/posts/10153664894881749',
  150. 'md5': '97ba073838964d12c70566e0085c2b91',
  151. 'info_dict': {
  152. 'id': '10153664894881749',
  153. 'ext': 'mp4',
  154. 'title': 'Average time to confirm recent Supreme Court nominees: 67 days Longest it\'s t...',
  155. 'thumbnail': r're:^https?://.*',
  156. 'timestamp': 1456259628,
  157. 'upload_date': '20160223',
  158. 'uploader': 'Barack Obama',
  159. },
  160. 'skip': 'Gif on giphy.com gone',
  161. }, {
  162. # have 1080P, but only up to 720p in swf params
  163. # data.video.story.attachments[].media
  164. 'url': 'https://www.facebook.com/cnn/videos/10155529876156509/',
  165. 'md5': 'ca63897a90c9452efee5f8c40d080e25',
  166. 'info_dict': {
  167. 'id': '10155529876156509',
  168. 'ext': 'mp4',
  169. 'title': 'Holocaust survivor becomes US citizen',
  170. 'description': 'She survived the holocaust — and years later, she’s getting her citizenship so she can vote for Hillary Clinton http://cnn.it/2eERh5f',
  171. 'timestamp': 1477818095,
  172. 'upload_date': '20161030',
  173. 'uploader': 'CNN',
  174. 'thumbnail': r're:^https?://.*',
  175. 'view_count': int,
  176. 'uploader_id': '100059479812265',
  177. 'concurrent_view_count': int,
  178. 'duration': 44.478,
  179. },
  180. }, {
  181. # bigPipe.onPageletArrive ... onPageletArrive pagelet_group_mall
  182. # data.node.comet_sections.content.story.attachments[].style_type_renderer.attachment.media
  183. 'url': 'https://www.facebook.com/yaroslav.korpan/videos/1417995061575415/',
  184. 'info_dict': {
  185. 'id': '1417995061575415',
  186. 'ext': 'mp4',
  187. 'title': 'Довгоочікуване відео | By Yaroslav - Facebook',
  188. 'description': 'Довгоочікуване відео',
  189. 'timestamp': 1486648217,
  190. 'upload_date': '20170209',
  191. 'uploader': 'Yaroslav Korpan',
  192. 'uploader_id': 'pfbid06AScABAWcW91qpiuGrLt99Ef9tvwHoXP6t8KeFYEqkSfreMtfa9nTveh8b2ZEVSWl',
  193. 'concurrent_view_count': int,
  194. 'thumbnail': r're:^https?://.*',
  195. 'view_count': int,
  196. 'duration': 11736.446,
  197. },
  198. 'params': {
  199. 'skip_download': True,
  200. },
  201. }, {
  202. # FIXME: Cannot parse data error
  203. 'url': 'https://www.facebook.com/LaGuiaDelVaron/posts/1072691702860471',
  204. 'info_dict': {
  205. 'id': '1072691702860471',
  206. 'ext': 'mp4',
  207. 'title': 'md5:ae2d22a93fbb12dad20dc393a869739d',
  208. 'timestamp': 1477305000,
  209. 'upload_date': '20161024',
  210. 'uploader': 'La Guía Del Varón',
  211. 'thumbnail': r're:^https?://.*',
  212. },
  213. 'skip': 'Requires logging in',
  214. }, {
  215. # data.node.comet_sections.content.story.attachments[].style_type_renderer.attachment.media
  216. 'url': 'https://www.facebook.com/groups/1024490957622648/permalink/1396382447100162/',
  217. 'info_dict': {
  218. 'id': '202882990186699',
  219. 'ext': 'mp4',
  220. 'title': 'birb (O v O") | Hello? Yes your uber ride is here',
  221. 'description': 'Hello? Yes your uber ride is here * Jukin Media Verified * Find this video and others like it by visiting...',
  222. 'timestamp': 1486035513,
  223. 'upload_date': '20170202',
  224. 'uploader': 'Elisabeth Ahtn',
  225. 'uploader_id': '100013949973717',
  226. },
  227. 'skip': 'Requires logging in',
  228. }, {
  229. # data.node.comet_sections.content.story.attachments[].throwbackStyles.attachment_target_renderer.attachment.target.attachments[].styles.attachment.media
  230. 'url': 'https://www.facebook.com/groups/1645456212344334/posts/3737828833107051/',
  231. 'info_dict': {
  232. 'id': '1569199726448814',
  233. 'ext': 'mp4',
  234. 'title': 'Pence MUST GO!',
  235. 'description': 'Vickie Gentry shared a memory.',
  236. 'timestamp': 1511548260,
  237. 'upload_date': '20171124',
  238. 'uploader': 'Vickie Gentry',
  239. 'uploader_id': 'pfbid0FuZhHCeWDAxWxEbr3yKPFaRstXvRxgsp9uCPG6GjD4J2AitB35NUAuJ4Q75KcjiDl',
  240. 'thumbnail': r're:^https?://.*',
  241. 'duration': 148.435,
  242. },
  243. }, {
  244. # data.node.comet_sections.content.story.attachments[].styles.attachment.media
  245. 'url': 'https://www.facebook.com/attn/posts/pfbid0j1Czf2gGDVqeQ8KiMLFm3pWN8GxsQmeRrVhimWDzMuKQoR8r4b1knNsejELmUgyhl',
  246. 'info_dict': {
  247. 'id': '6968553779868435',
  248. 'ext': 'mp4',
  249. 'description': 'md5:2f2fcf93e97ac00244fe64521bbdb0cb',
  250. 'uploader': 'ATTN:',
  251. 'upload_date': '20231207',
  252. 'title': 'ATTN:',
  253. 'duration': 132.675,
  254. 'uploader_id': '100064451419378',
  255. 'view_count': int,
  256. 'thumbnail': r're:^https?://.*',
  257. 'timestamp': 1701975646,
  258. },
  259. }, {
  260. # data.node.comet_sections.content.story.attachments[].styles.attachment.media
  261. 'url': 'https://www.facebook.com/permalink.php?story_fbid=pfbid0fqQuVEQyXRa9Dp4RcaTR14KHU3uULHV1EK7eckNXSH63JMuoALsAvVCJ97zAGitil&id=100068861234290',
  262. 'info_dict': {
  263. 'id': '270103405756416',
  264. 'ext': 'mp4',
  265. 'title': 'Lela Evans',
  266. 'description': 'Today Makkovik\'s own Pilot Mandy Smith made her inaugural landing on the airstrip in her hometown. What a proud moment as we all cheered and...',
  267. 'thumbnail': r're:^https?://.*',
  268. 'uploader': 'Lela Evans',
  269. 'uploader_id': 'pfbid0shZJipuigyy5mqrUJn9ub5LJFWNHvan5prtyi3LrDuuuJ4NwrURgnQHYR9fywBepl',
  270. 'upload_date': '20231228',
  271. 'timestamp': 1703804085,
  272. 'duration': 394.347,
  273. 'view_count': int,
  274. },
  275. }, {
  276. 'url': 'https://www.facebook.com/story.php?story_fbid=pfbid0Fnzhm8UuzjBYpPMNFzaSpFE9UmLdU4fJN8qTANi1Dmtj5q7DNrL5NERXfsAzDEV7l&id=100073071055552',
  277. 'only_matching': True,
  278. }, {
  279. 'url': 'https://www.facebook.com/video.php?v=10204634152394104',
  280. 'only_matching': True,
  281. }, {
  282. 'url': 'https://www.facebook.com/amogood/videos/1618742068337349/?fref=nf',
  283. 'only_matching': True,
  284. }, {
  285. # data.mediaset.currMedia.edges
  286. 'url': 'https://www.facebook.com/ChristyClarkForBC/videos/vb.22819070941/10153870694020942/?type=2&theater',
  287. 'only_matching': True,
  288. }, {
  289. # data.video.story.attachments[].media
  290. 'url': 'facebook:544765982287235',
  291. 'only_matching': True,
  292. }, {
  293. # data.node.comet_sections.content.story.attachments[].style_type_renderer.attachment.media
  294. 'url': 'https://www.facebook.com/groups/164828000315060/permalink/764967300301124/',
  295. 'only_matching': True,
  296. }, {
  297. # data.video.creation_story.attachments[].media
  298. 'url': 'https://zh-hk.facebook.com/peoplespower/videos/1135894589806027/',
  299. 'only_matching': True,
  300. }, {
  301. # data.video
  302. 'url': 'https://www.facebookwkhpilnemxj7asaniu7vnjjbiltxjqhye3mhbshg7kx5tfyd.onion/video.php?v=274175099429670',
  303. 'only_matching': True,
  304. }, {
  305. # no title
  306. 'url': 'https://www.facebook.com/onlycleverentertainment/videos/1947995502095005/',
  307. 'only_matching': True,
  308. }, {
  309. # data.video
  310. 'url': 'https://www.facebook.com/WatchESLOne/videos/359649331226507/',
  311. 'info_dict': {
  312. 'id': '359649331226507',
  313. 'ext': 'mp4',
  314. 'title': 'Fnatic vs. EG - Group A - Opening Match - ESL One Birmingham Day 1',
  315. 'description': '#ESLOne VoD - Birmingham Finals Day#1 Fnatic vs. @Evil Geniuses',
  316. 'timestamp': 1527084179,
  317. 'upload_date': '20180523',
  318. 'uploader': 'ESL One Dota 2',
  319. 'uploader_id': '100066514874195',
  320. 'duration': 4524.212,
  321. 'view_count': int,
  322. 'thumbnail': r're:^https?://.*',
  323. 'concurrent_view_count': int,
  324. },
  325. 'params': {
  326. 'skip_download': True,
  327. },
  328. }, {
  329. # data.node.comet_sections.content.story.attachments[].style_type_renderer.attachment.all_subattachments.nodes[].media
  330. 'url': 'https://www.facebook.com/100033620354545/videos/106560053808006/',
  331. 'info_dict': {
  332. 'id': '106560053808006',
  333. 'ext': 'mp4',
  334. 'title': 'Josef',
  335. 'thumbnail': r're:^https?://.*',
  336. 'concurrent_view_count': int,
  337. 'uploader_id': 'pfbid0cibUN6tV7DYgdbJdsUFN46wc4jKpVSPAvJQhFofGqBGmVn3V3JtAs2tfUwziw2hUl',
  338. 'timestamp': 1549275572,
  339. 'duration': 3.413,
  340. 'uploader': 'Josef Novak',
  341. 'description': '',
  342. 'upload_date': '20190204',
  343. },
  344. }, {
  345. # data.video.story.attachments[].media
  346. 'url': 'https://www.facebook.com/watch/?v=647537299265662',
  347. 'only_matching': True,
  348. }, {
  349. # FIXME: https://github.com/yt-dlp/yt-dlp/issues/542
  350. # data.node.comet_sections.content.story.attachments[].style_type_renderer.attachment.all_subattachments.nodes[].media
  351. 'url': 'https://www.facebook.com/PankajShahLondon/posts/10157667649866271',
  352. 'info_dict': {
  353. 'id': '10157667649866271',
  354. },
  355. 'playlist_count': 3,
  356. 'skip': 'Requires logging in',
  357. }, {
  358. # data.nodes[].comet_sections.content.story.attachments[].style_type_renderer.attachment.media
  359. 'url': 'https://m.facebook.com/Alliance.Police.Department/posts/4048563708499330',
  360. 'info_dict': {
  361. 'id': '117576630041613',
  362. 'ext': 'mp4',
  363. # TODO: title can be extracted from video page
  364. 'title': 'Facebook video #117576630041613',
  365. 'uploader_id': '189393014416438',
  366. 'upload_date': '20201123',
  367. 'timestamp': 1606162592,
  368. },
  369. 'skip': 'Requires logging in',
  370. }, {
  371. # node.comet_sections.content.story.attached_story.attachments.style_type_renderer.attachment.media
  372. 'url': 'https://www.facebook.com/groups/ateistiskselskab/permalink/10154930137678856/',
  373. 'info_dict': {
  374. 'id': '211567722618337',
  375. 'ext': 'mp4',
  376. 'title': 'Facebook video #211567722618337',
  377. 'uploader_id': '127875227654254',
  378. 'upload_date': '20161122',
  379. 'timestamp': 1479793574,
  380. },
  381. 'skip': 'No video',
  382. }, {
  383. # data.video.creation_story.attachments[].media
  384. 'url': 'https://www.facebook.com/watch/live/?v=1823658634322275',
  385. 'only_matching': True,
  386. }, {
  387. 'url': 'https://www.facebook.com/watchparty/211641140192478',
  388. 'info_dict': {
  389. 'id': '211641140192478',
  390. },
  391. 'playlist_count': 1,
  392. 'skip': 'Requires logging in',
  393. }, {
  394. # data.event.cover_media_renderer.cover_video
  395. 'url': 'https://m.facebook.com/events/1509582499515440',
  396. 'info_dict': {
  397. 'id': '637246984455045',
  398. 'ext': 'mp4',
  399. 'title': 'ANALISI IN CAMPO OSCURO " Coaguli nel sangue dei vaccinati"',
  400. 'description': 'Other event by Comitato Liberi Pensatori on Tuesday, October 18 2022',
  401. 'thumbnail': r're:^https?://.*',
  402. 'uploader': 'Comitato Liberi Pensatori',
  403. 'uploader_id': '100065709540881',
  404. },
  405. }]
  406. _SUPPORTED_PAGLETS_REGEX = r'(?:pagelet_group_mall|permalink_video_pagelet|hyperfeed_story_id_[0-9a-f]+)'
  407. _api_config = {
  408. 'graphURI': '/api/graphql/',
  409. }
  410. def _perform_login(self, username, password):
  411. login_page_req = Request(self._LOGIN_URL)
  412. self._set_cookie('facebook.com', 'locale', 'en_US')
  413. login_page = self._download_webpage(login_page_req, None,
  414. note='Downloading login page',
  415. errnote='Unable to download login page')
  416. lsd = self._search_regex(
  417. r'<input type="hidden" name="lsd" value="([^"]*)"',
  418. login_page, 'lsd')
  419. lgnrnd = self._search_regex(r'name="lgnrnd" value="([^"]*?)"', login_page, 'lgnrnd')
  420. login_form = {
  421. 'email': username,
  422. 'pass': password,
  423. 'lsd': lsd,
  424. 'lgnrnd': lgnrnd,
  425. 'next': 'http://facebook.com/home.php',
  426. 'default_persistent': '0',
  427. 'legacy_return': '1',
  428. 'timezone': '-60',
  429. 'trynum': '1',
  430. }
  431. request = Request(self._LOGIN_URL, urlencode_postdata(login_form))
  432. request.headers['Content-Type'] = 'application/x-www-form-urlencoded'
  433. try:
  434. login_results = self._download_webpage(request, None,
  435. note='Logging in', errnote='unable to fetch login page')
  436. if re.search(r'<form(.*)name="login"(.*)</form>', login_results) is not None:
  437. error = self._html_search_regex(
  438. r'(?s)<div[^>]+class=(["\']).*?login_error_box.*?\1[^>]*><div[^>]*>.*?</div><div[^>]*>(?P<error>.+?)</div>',
  439. login_results, 'login error', default=None, group='error')
  440. if error:
  441. raise ExtractorError(f'Unable to login: {error}', expected=True)
  442. self.report_warning('unable to log in: bad username/password, or exceeded login rate limit (~3/min). Check credentials or wait.')
  443. return
  444. fb_dtsg = self._search_regex(
  445. r'name="fb_dtsg" value="(.+?)"', login_results, 'fb_dtsg', default=None)
  446. h = self._search_regex(
  447. r'name="h"\s+(?:\w+="[^"]+"\s+)*?value="([^"]+)"', login_results, 'h', default=None)
  448. if not fb_dtsg or not h:
  449. return
  450. check_form = {
  451. 'fb_dtsg': fb_dtsg,
  452. 'h': h,
  453. 'name_action_selected': 'dont_save',
  454. }
  455. check_req = Request(self._CHECKPOINT_URL, urlencode_postdata(check_form))
  456. check_req.headers['Content-Type'] = 'application/x-www-form-urlencoded'
  457. check_response = self._download_webpage(check_req, None,
  458. note='Confirming login')
  459. if re.search(r'id="checkpointSubmitButton"', check_response) is not None:
  460. self.report_warning('Unable to confirm login, you have to login in your browser and authorize the login.')
  461. except network_exceptions as err:
  462. self.report_warning(f'unable to log in: {err}')
  463. return
  464. def _extract_from_url(self, url, video_id):
  465. webpage = self._download_webpage(
  466. url.replace('://m.facebook.com/', '://www.facebook.com/'), video_id)
  467. def extract_metadata(webpage):
  468. post_data = [self._parse_json(j, video_id, fatal=False) for j in re.findall(
  469. r'data-sjs>({.*?ScheduledServerJS.*?})</script>', webpage)]
  470. post = traverse_obj(post_data, (
  471. ..., 'require', ..., ..., ..., '__bbox', 'require', ..., ..., ..., '__bbox', 'result', 'data'), expected_type=dict) or []
  472. media = traverse_obj(post, (..., 'attachments', ..., lambda k, v: (
  473. k == 'media' and str(v['id']) == video_id and v['__typename'] == 'Video')), expected_type=dict)
  474. title = get_first(media, ('title', 'text'))
  475. description = get_first(media, ('creation_story', 'comet_sections', 'message', 'story', 'message', 'text'))
  476. page_title = title or self._html_search_regex((
  477. r'<h2\s+[^>]*class="uiHeaderTitle"[^>]*>(?P<content>[^<]*)</h2>',
  478. r'(?s)<span class="fbPhotosPhotoCaption".*?id="fbPhotoPageCaption"><span class="hasCaption">(?P<content>.*?)</span>',
  479. self._meta_regex('og:title'), self._meta_regex('twitter:title'), r'<title>(?P<content>.+?)</title>',
  480. ), webpage, 'title', default=None, group='content')
  481. description = description or self._html_search_meta(
  482. ['description', 'og:description', 'twitter:description'],
  483. webpage, 'description', default=None)
  484. uploader_data = (
  485. get_first(media, ('owner', {dict}))
  486. or get_first(post, ('video', 'creation_story', 'attachments', ..., 'media', lambda k, v: k == 'owner' and v['name']))
  487. or get_first(post, (..., 'video', lambda k, v: k == 'owner' and v['name']))
  488. or get_first(post, ('node', 'actors', ..., {dict}))
  489. or get_first(post, ('event', 'event_creator', {dict})) or {})
  490. uploader = uploader_data.get('name') or (
  491. clean_html(get_element_by_id('fbPhotoPageAuthorName', webpage))
  492. or self._search_regex(
  493. (r'ownerName\s*:\s*"([^"]+)"', *self._og_regexes('title')), webpage, 'uploader', fatal=False))
  494. timestamp = int_or_none(self._search_regex(
  495. r'<abbr[^>]+data-utime=["\'](\d+)', webpage,
  496. 'timestamp', default=None))
  497. thumbnail = self._html_search_meta(
  498. ['og:image', 'twitter:image'], webpage, 'thumbnail', default=None)
  499. # some webpages contain unretrievable thumbnail urls
  500. # like https://lookaside.fbsbx.com/lookaside/crawler/media/?media_id=10155168902769113&get_thumbnail=1
  501. # in https://www.facebook.com/yaroslav.korpan/videos/1417995061575415/
  502. if thumbnail and not re.search(r'\.(?:jpg|png)', thumbnail):
  503. thumbnail = None
  504. info_dict = {
  505. 'description': description,
  506. 'uploader': uploader,
  507. 'uploader_id': uploader_data.get('id'),
  508. 'timestamp': timestamp,
  509. 'thumbnail': thumbnail,
  510. 'view_count': parse_count(self._search_regex(
  511. (r'\bviewCount\s*:\s*["\']([\d,.]+)', r'video_view_count["\']\s*:\s*(\d+)'),
  512. webpage, 'view count', default=None)),
  513. 'concurrent_view_count': get_first(post, (
  514. ('video', (..., ..., 'attachments', ..., 'media')), 'liveViewerCount', {int_or_none})),
  515. }
  516. info_json_ld = self._search_json_ld(webpage, video_id, default={})
  517. info_json_ld['title'] = (re.sub(r'\s*\|\s*Facebook$', '', title or info_json_ld.get('title') or page_title or '')
  518. or (description or '').replace('\n', ' ') or f'Facebook video #{video_id}')
  519. return merge_dicts(info_json_ld, info_dict)
  520. video_data = None
  521. def extract_video_data(instances):
  522. video_data = []
  523. for item in instances:
  524. if try_get(item, lambda x: x[1][0]) == 'VideoConfig':
  525. video_item = item[2][0]
  526. if video_item.get('video_id'):
  527. video_data.append(video_item['videoData'])
  528. return video_data
  529. server_js_data = self._parse_json(self._search_regex(
  530. [r'handleServerJS\(({.+})(?:\);|,")', r'\bs\.handle\(({.+?})\);'],
  531. webpage, 'server js data', default='{}'), video_id, fatal=False)
  532. if server_js_data:
  533. video_data = extract_video_data(server_js_data.get('instances', []))
  534. def extract_from_jsmods_instances(js_data):
  535. if js_data:
  536. return extract_video_data(try_get(
  537. js_data, lambda x: x['jsmods']['instances'], list) or [])
  538. def extract_dash_manifest(video, formats):
  539. dash_manifest = traverse_obj(video, 'dash_manifest', 'playlist', expected_type=str)
  540. if dash_manifest:
  541. formats.extend(self._parse_mpd_formats(
  542. compat_etree_fromstring(urllib.parse.unquote_plus(dash_manifest)),
  543. mpd_url=video.get('dash_manifest_url')))
  544. def process_formats(info):
  545. # Downloads with browser's User-Agent are rate limited. Working around
  546. # with non-browser User-Agent.
  547. for f in info['formats']:
  548. # Downloads with browser's User-Agent are rate limited. Working around
  549. # with non-browser User-Agent.
  550. f.setdefault('http_headers', {})['User-Agent'] = 'facebookexternalhit/1.1'
  551. # Formats larger than ~500MB will return error 403 unless chunk size is regulated
  552. f.setdefault('downloader_options', {})['http_chunk_size'] = 250 << 20
  553. def extract_relay_data(_filter):
  554. return self._parse_json(self._search_regex(
  555. rf'data-sjs>({{.*?{_filter}.*?}})</script>',
  556. webpage, 'replay data', default='{}'), video_id, fatal=False) or {}
  557. def extract_relay_prefetched_data(_filter):
  558. return traverse_obj(extract_relay_data(_filter), (
  559. 'require', (None, (..., ..., ..., '__bbox', 'require')),
  560. lambda _, v: any(key.startswith('RelayPrefetchedStreamCache') for key in v),
  561. ..., ..., '__bbox', 'result', 'data', {dict}), get_all=False) or {}
  562. if not video_data:
  563. server_js_data = self._parse_json(self._search_regex([
  564. r'bigPipe\.onPageletArrive\(({.+?})\)\s*;\s*}\s*\)\s*,\s*["\']onPageletArrive\s+' + self._SUPPORTED_PAGLETS_REGEX,
  565. rf'bigPipe\.onPageletArrive\(({{.*?id\s*:\s*"{self._SUPPORTED_PAGLETS_REGEX}".*?}})\);',
  566. ], webpage, 'js data', default='{}'), video_id, js_to_json, False)
  567. video_data = extract_from_jsmods_instances(server_js_data)
  568. if not video_data:
  569. data = extract_relay_prefetched_data(
  570. r'"(?:dash_manifest|playable_url(?:_quality_hd)?)')
  571. if data:
  572. entries = []
  573. def parse_graphql_video(video):
  574. v_id = video.get('videoId') or video.get('id') or video_id
  575. reel_info = traverse_obj(
  576. video, ('creation_story', 'short_form_video_context', 'playback_video', {dict}))
  577. if reel_info:
  578. video = video['creation_story']
  579. video['owner'] = traverse_obj(video, ('short_form_video_context', 'video_owner'))
  580. video.update(reel_info)
  581. formats = []
  582. q = qualities(['sd', 'hd'])
  583. for key, format_id in (('playable_url', 'sd'), ('playable_url_quality_hd', 'hd'),
  584. ('playable_url_dash', ''), ('browser_native_hd_url', 'hd'),
  585. ('browser_native_sd_url', 'sd')):
  586. playable_url = video.get(key)
  587. if not playable_url:
  588. continue
  589. if determine_ext(playable_url) == 'mpd':
  590. formats.extend(self._extract_mpd_formats(playable_url, video_id))
  591. else:
  592. formats.append({
  593. 'format_id': format_id,
  594. # sd, hd formats w/o resolution info should be deprioritized below DASH
  595. 'quality': q(format_id) - 3,
  596. 'url': playable_url,
  597. })
  598. extract_dash_manifest(video, formats)
  599. if not formats:
  600. # Do not append false positive entry w/o any formats
  601. return
  602. automatic_captions, subtitles = {}, {}
  603. is_broadcast = traverse_obj(video, ('is_video_broadcast', {bool}))
  604. for caption in traverse_obj(video, (
  605. 'video_available_captions_locales',
  606. {lambda x: sorted(x, key=lambda c: c['locale'])},
  607. lambda _, v: url_or_none(v['captions_url']),
  608. )):
  609. lang = caption.get('localized_language') or 'und'
  610. subs = {
  611. 'url': caption['captions_url'],
  612. 'name': format_field(caption, 'localized_country', f'{lang} (%s)', default=lang),
  613. }
  614. if caption.get('localized_creation_method') or is_broadcast:
  615. automatic_captions.setdefault(caption['locale'], []).append(subs)
  616. else:
  617. subtitles.setdefault(caption['locale'], []).append(subs)
  618. captions_url = traverse_obj(video, ('captions_url', {url_or_none}))
  619. if captions_url and not automatic_captions and not subtitles:
  620. locale = self._html_search_meta(
  621. ['og:locale', 'twitter:locale'], webpage, 'locale', default='en_US')
  622. (automatic_captions if is_broadcast else subtitles)[locale] = [{'url': captions_url}]
  623. info = {
  624. 'id': v_id,
  625. 'formats': formats,
  626. 'thumbnail': traverse_obj(
  627. video, ('thumbnailImage', 'uri'), ('preferred_thumbnail', 'image', 'uri')),
  628. 'uploader_id': traverse_obj(video, ('owner', 'id', {str_or_none})),
  629. 'timestamp': traverse_obj(video, 'publish_time', 'creation_time', expected_type=int_or_none),
  630. 'duration': (float_or_none(video.get('playable_duration_in_ms'), 1000)
  631. or float_or_none(video.get('length_in_second'))),
  632. 'automatic_captions': automatic_captions,
  633. 'subtitles': subtitles,
  634. }
  635. process_formats(info)
  636. description = try_get(video, lambda x: x['savable_description']['text'])
  637. title = video.get('name')
  638. if title:
  639. info.update({
  640. 'title': title,
  641. 'description': description,
  642. })
  643. else:
  644. info['title'] = description or f'Facebook video #{v_id}'
  645. entries.append(info)
  646. def parse_attachment(attachment, key='media'):
  647. media = attachment.get(key) or {}
  648. if media.get('__typename') == 'Video':
  649. return parse_graphql_video(media)
  650. nodes = variadic(traverse_obj(data, 'nodes', 'node') or [])
  651. attachments = traverse_obj(nodes, (
  652. ..., 'comet_sections', 'content', 'story', (None, 'attached_story'), 'attachments',
  653. ..., ('styles', 'style_type_renderer', ('throwbackStyles', 'attachment_target_renderer')),
  654. 'attachment', {dict}))
  655. for attachment in attachments:
  656. ns = traverse_obj(attachment, ('all_subattachments', 'nodes', ..., {dict}),
  657. ('target', 'attachments', ..., 'styles', 'attachment', {dict}))
  658. for n in ns:
  659. parse_attachment(n)
  660. parse_attachment(attachment)
  661. edges = try_get(data, lambda x: x['mediaset']['currMedia']['edges'], list) or []
  662. for edge in edges:
  663. parse_attachment(edge, key='node')
  664. video = traverse_obj(data, (
  665. 'event', 'cover_media_renderer', 'cover_video'), 'video', expected_type=dict) or {}
  666. if video:
  667. attachments = try_get(video, [
  668. lambda x: x['story']['attachments'],
  669. lambda x: x['creation_story']['attachments'],
  670. ], list) or []
  671. for attachment in attachments:
  672. parse_attachment(attachment)
  673. if not entries:
  674. parse_graphql_video(video)
  675. if len(entries) > 1:
  676. return self.playlist_result(entries, video_id)
  677. video_info = entries[0] if entries else {'id': video_id}
  678. webpage_info = extract_metadata(webpage)
  679. # honor precise duration in video info
  680. if video_info.get('duration'):
  681. webpage_info['duration'] = video_info['duration']
  682. # preserve preferred_thumbnail in video info
  683. if video_info.get('thumbnail'):
  684. webpage_info['thumbnail'] = video_info['thumbnail']
  685. return merge_dicts(webpage_info, video_info)
  686. if not video_data:
  687. m_msg = re.search(r'class="[^"]*uiInterstitialContent[^"]*"><div>(.*?)</div>', webpage)
  688. if m_msg is not None:
  689. raise ExtractorError(
  690. f'The video is not available, Facebook said: "{m_msg.group(1)}"',
  691. expected=True)
  692. elif any(p in webpage for p in (
  693. '>You must log in to continue',
  694. 'id="login_form"',
  695. 'id="loginbutton"')):
  696. self.raise_login_required()
  697. if not video_data and '/watchparty/' in url:
  698. post_data = {
  699. 'doc_id': 3731964053542869,
  700. 'variables': json.dumps({
  701. 'livingRoomID': video_id,
  702. }),
  703. }
  704. prefetched_data = extract_relay_prefetched_data(r'"login_data"\s*:\s*{')
  705. if prefetched_data:
  706. lsd = try_get(prefetched_data, lambda x: x['login_data']['lsd'], dict)
  707. if lsd:
  708. post_data[lsd['name']] = lsd['value']
  709. relay_data = extract_relay_data(r'\[\s*"RelayAPIConfigDefaults"\s*,')
  710. for define in (relay_data.get('define') or []):
  711. if define[0] == 'RelayAPIConfigDefaults':
  712. self._api_config = define[2]
  713. living_room = self._download_json(
  714. urljoin(url, self._api_config['graphURI']), video_id,
  715. data=urlencode_postdata(post_data))['data']['living_room']
  716. entries = []
  717. for edge in (try_get(living_room, lambda x: x['recap']['watched_content']['edges']) or []):
  718. video = try_get(edge, lambda x: x['node']['video']) or {}
  719. v_id = video.get('id')
  720. if not v_id:
  721. continue
  722. v_id = str(v_id)
  723. entries.append(self.url_result(
  724. self._VIDEO_PAGE_TEMPLATE % v_id,
  725. self.ie_key(), v_id, video.get('name')))
  726. return self.playlist_result(entries, video_id)
  727. if not video_data:
  728. # Video info not in first request, do a secondary request using
  729. # tahoe player specific URL
  730. tahoe_data = self._download_webpage(
  731. self._VIDEO_PAGE_TAHOE_TEMPLATE % video_id, video_id,
  732. data=urlencode_postdata({
  733. '__a': 1,
  734. '__pc': self._search_regex(
  735. r'pkg_cohort["\']\s*:\s*["\'](.+?)["\']', webpage,
  736. 'pkg cohort', default='PHASED:DEFAULT'),
  737. '__rev': self._search_regex(
  738. r'client_revision["\']\s*:\s*(\d+),', webpage,
  739. 'client revision', default='3944515'),
  740. 'fb_dtsg': self._search_regex(
  741. r'"DTSGInitialData"\s*,\s*\[\]\s*,\s*{\s*"token"\s*:\s*"([^"]+)"',
  742. webpage, 'dtsg token', default=''),
  743. }),
  744. headers={
  745. 'Content-Type': 'application/x-www-form-urlencoded',
  746. })
  747. tahoe_js_data = self._parse_json(
  748. self._search_regex(
  749. r'for\s+\(\s*;\s*;\s*\)\s*;(.+)', tahoe_data,
  750. 'tahoe js data', default='{}'),
  751. video_id, fatal=False)
  752. video_data = extract_from_jsmods_instances(tahoe_js_data)
  753. if not video_data:
  754. raise ExtractorError('Cannot parse data')
  755. if len(video_data) > 1:
  756. entries = []
  757. for v in video_data:
  758. video_url = v[0].get('video_url')
  759. if not video_url:
  760. continue
  761. entries.append(self.url_result(urljoin(
  762. url, video_url), self.ie_key(), v[0].get('video_id')))
  763. return self.playlist_result(entries, video_id)
  764. video_data = video_data[0]
  765. formats = []
  766. subtitles = {}
  767. for f in video_data:
  768. format_id = f['stream_type']
  769. if f and isinstance(f, dict):
  770. f = [f]
  771. if not f or not isinstance(f, list):
  772. continue
  773. for quality in ('sd', 'hd'):
  774. for src_type in ('src', 'src_no_ratelimit'):
  775. src = f[0].get(f'{quality}_{src_type}')
  776. if src:
  777. # sd, hd formats w/o resolution info should be deprioritized below DASH
  778. # TODO: investigate if progressive or src formats still exist
  779. preference = -10 if format_id == 'progressive' else -3
  780. if quality == 'hd':
  781. preference += 1
  782. formats.append({
  783. 'format_id': f'{format_id}_{quality}_{src_type}',
  784. 'url': src,
  785. 'quality': preference,
  786. 'height': 720 if quality == 'hd' else None,
  787. })
  788. extract_dash_manifest(f[0], formats)
  789. subtitles_src = f[0].get('subtitles_src')
  790. if subtitles_src:
  791. subtitles.setdefault('en', []).append({'url': subtitles_src})
  792. info_dict = {
  793. 'id': video_id,
  794. 'formats': formats,
  795. 'subtitles': subtitles,
  796. }
  797. process_formats(info_dict)
  798. info_dict.update(extract_metadata(webpage))
  799. return info_dict
  800. def _real_extract(self, url):
  801. video_id = self._match_id(url)
  802. real_url = self._VIDEO_PAGE_TEMPLATE % video_id if url.startswith('facebook:') else url
  803. return self._extract_from_url(real_url, video_id)
  804. class FacebookPluginsVideoIE(InfoExtractor):
  805. _VALID_URL = r'https?://(?:[\w-]+\.)?facebook\.com/plugins/video\.php\?.*?\bhref=(?P<id>https.+)'
  806. _TESTS = [{
  807. 'url': 'https://www.facebook.com/plugins/video.php?href=https%3A%2F%2Fwww.facebook.com%2Fgov.sg%2Fvideos%2F10154383743583686%2F&show_text=0&width=560',
  808. 'md5': '5954e92cdfe51fe5782ae9bda7058a07',
  809. 'info_dict': {
  810. 'id': '10154383743583686',
  811. 'ext': 'mp4',
  812. # TODO: Fix title, uploader
  813. 'title': 'What to do during the haze?',
  814. 'uploader': 'Gov.sg',
  815. 'upload_date': '20160826',
  816. 'timestamp': 1472184808,
  817. },
  818. 'add_ie': [FacebookIE.ie_key()],
  819. }, {
  820. 'url': 'https://www.facebook.com/plugins/video.php?href=https%3A%2F%2Fwww.facebook.com%2Fvideo.php%3Fv%3D10204634152394104',
  821. 'only_matching': True,
  822. }, {
  823. 'url': 'https://www.facebook.com/plugins/video.php?href=https://www.facebook.com/gov.sg/videos/10154383743583686/&show_text=0&width=560',
  824. 'only_matching': True,
  825. }]
  826. def _real_extract(self, url):
  827. return self.url_result(
  828. urllib.parse.unquote(self._match_id(url)),
  829. FacebookIE.ie_key())
  830. class FacebookRedirectURLIE(InfoExtractor):
  831. IE_DESC = False # Do not list
  832. _VALID_URL = r'https?://(?:[\w-]+\.)?facebook\.com/flx/warn[/?]'
  833. _TESTS = [{
  834. 'url': 'https://www.facebook.com/flx/warn/?h=TAQHsoToz&u=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DpO8h3EaFRdo&s=1',
  835. 'info_dict': {
  836. 'id': 'pO8h3EaFRdo',
  837. 'ext': 'mp4',
  838. 'title': 'Tripeo Boiler Room x Dekmantel Festival DJ Set',
  839. 'description': 'md5:2d713ccbb45b686a1888397b2c77ca6b',
  840. 'channel_id': 'UCGBpxWJr9FNOcFYA5GkKrMg',
  841. 'playable_in_embed': True,
  842. 'categories': ['Music'],
  843. 'channel': 'Boiler Room',
  844. 'uploader_id': 'brtvofficial',
  845. 'uploader': 'Boiler Room',
  846. 'tags': 'count:11',
  847. 'duration': 3332,
  848. 'live_status': 'not_live',
  849. 'thumbnail': 'https://i.ytimg.com/vi/pO8h3EaFRdo/maxresdefault.jpg',
  850. 'channel_url': 'https://www.youtube.com/channel/UCGBpxWJr9FNOcFYA5GkKrMg',
  851. 'availability': 'public',
  852. 'uploader_url': 'http://www.youtube.com/user/brtvofficial',
  853. 'upload_date': '20150917',
  854. 'age_limit': 0,
  855. 'view_count': int,
  856. 'like_count': int,
  857. },
  858. 'add_ie': ['Youtube'],
  859. 'params': {'skip_download': 'Youtube'},
  860. }]
  861. def _real_extract(self, url):
  862. redirect_url = url_or_none(parse_qs(url).get('u', [None])[-1])
  863. if not redirect_url:
  864. raise ExtractorError('Invalid facebook redirect URL', expected=True)
  865. return self.url_result(redirect_url)
  866. class FacebookReelIE(InfoExtractor):
  867. _VALID_URL = r'https?://(?:[\w-]+\.)?facebook\.com/reel/(?P<id>\d+)'
  868. IE_NAME = 'facebook:reel'
  869. _TESTS = [{
  870. 'url': 'https://www.facebook.com/reel/1195289147628387',
  871. 'md5': 'f13dd37f2633595982db5ed8765474d3',
  872. 'info_dict': {
  873. 'id': '1195289147628387',
  874. 'ext': 'mp4',
  875. 'title': 'md5:b05800b5b1ad56c0ca78bd3807b6a61e',
  876. 'description': 'md5:22f03309b216ac84720183961441d8db',
  877. 'uploader': 'md5:723e6cb3091241160f20b3c5dc282af1',
  878. 'uploader_id': '100040874179269',
  879. 'duration': 9.579,
  880. 'timestamp': 1637502609,
  881. 'upload_date': '20211121',
  882. 'thumbnail': r're:^https?://.*',
  883. },
  884. }]
  885. def _real_extract(self, url):
  886. video_id = self._match_id(url)
  887. return self.url_result(
  888. f'https://m.facebook.com/watch/?v={video_id}&_rdr', FacebookIE, video_id)
  889. class FacebookAdsIE(InfoExtractor):
  890. _VALID_URL = r'https?://(?:[\w-]+\.)?facebook\.com/ads/library/?\?(?:[^#]+&)?id=(?P<id>\d+)'
  891. IE_NAME = 'facebook:ads'
  892. _TESTS = [{
  893. 'url': 'https://www.facebook.com/ads/library/?id=899206155126718',
  894. 'info_dict': {
  895. 'id': '899206155126718',
  896. 'ext': 'mp4',
  897. 'title': 'video by Kandao',
  898. 'uploader': 'Kandao',
  899. 'uploader_id': '774114102743284',
  900. 'uploader_url': r're:^https?://.*',
  901. 'timestamp': 1702548330,
  902. 'thumbnail': r're:^https?://.*',
  903. 'upload_date': '20231214',
  904. 'like_count': int,
  905. },
  906. }, {
  907. 'url': 'https://www.facebook.com/ads/library/?id=893637265423481',
  908. 'info_dict': {
  909. 'id': '893637265423481',
  910. 'title': 'Jusqu\u2019\u00e0 -25% sur une s\u00e9lection de vins p\u00e9tillants italiens ',
  911. 'uploader': 'Eataly Paris Marais',
  912. 'uploader_id': '2086668958314152',
  913. 'uploader_url': r're:^https?://.*',
  914. 'timestamp': 1703571529,
  915. 'upload_date': '20231226',
  916. 'like_count': int,
  917. },
  918. 'playlist_count': 3,
  919. }, {
  920. 'url': 'https://es-la.facebook.com/ads/library/?id=901230958115569',
  921. 'only_matching': True,
  922. }, {
  923. 'url': 'https://m.facebook.com/ads/library/?id=901230958115569',
  924. 'only_matching': True,
  925. }]
  926. _FORMATS_MAP = {
  927. 'watermarked_video_sd_url': ('sd-wmk', 'SD, watermarked'),
  928. 'video_sd_url': ('sd', None),
  929. 'watermarked_video_hd_url': ('hd-wmk', 'HD, watermarked'),
  930. 'video_hd_url': ('hd', None),
  931. }
  932. def _extract_formats(self, video_dict):
  933. formats = []
  934. for format_key, format_url in traverse_obj(video_dict, (
  935. {dict.items}, lambda _, v: v[0] in self._FORMATS_MAP and url_or_none(v[1]),
  936. )):
  937. formats.append({
  938. 'format_id': self._FORMATS_MAP[format_key][0],
  939. 'format_note': self._FORMATS_MAP[format_key][1],
  940. 'url': format_url,
  941. 'ext': 'mp4',
  942. 'quality': qualities(tuple(self._FORMATS_MAP))(format_key),
  943. })
  944. return formats
  945. def _real_extract(self, url):
  946. video_id = self._match_id(url)
  947. webpage = self._download_webpage(url, video_id)
  948. post_data = [self._parse_json(j, video_id, fatal=False)
  949. for j in re.findall(r's\.handle\(({.*})\);requireLazy\(', webpage)]
  950. data = traverse_obj(post_data, (
  951. ..., 'require', ..., ..., ..., 'props', 'deeplinkAdCard', 'snapshot', {dict}), get_all=False)
  952. if not data:
  953. raise ExtractorError('Unable to extract ad data')
  954. title = data.get('title')
  955. if not title or title == '{{product.name}}':
  956. title = join_nonempty('display_format', 'page_name', delim=' by ', from_dict=data)
  957. info_dict = traverse_obj(data, {
  958. 'description': ('link_description', {str}, {lambda x: x if x != '{{product.description}}' else None}),
  959. 'uploader': ('page_name', {str}),
  960. 'uploader_id': ('page_id', {str_or_none}),
  961. 'uploader_url': ('page_profile_uri', {url_or_none}),
  962. 'timestamp': ('creation_time', {int_or_none}),
  963. 'like_count': ('page_like_count', {int_or_none}),
  964. })
  965. entries = []
  966. for idx, entry in enumerate(traverse_obj(
  967. data, (('videos', 'cards'), lambda _, v: any(url_or_none(v[f]) for f in self._FORMATS_MAP))), 1,
  968. ):
  969. entries.append({
  970. 'id': f'{video_id}_{idx}',
  971. 'title': entry.get('title') or title,
  972. 'description': entry.get('link_description') or info_dict.get('description'),
  973. 'thumbnail': url_or_none(entry.get('video_preview_image_url')),
  974. 'formats': self._extract_formats(entry),
  975. })
  976. if len(entries) == 1:
  977. info_dict.update(entries[0])
  978. elif len(entries) > 1:
  979. info_dict.update({
  980. 'title': entries[0]['title'],
  981. 'entries': entries,
  982. '_type': 'playlist',
  983. })
  984. info_dict['id'] = video_id
  985. return info_dict