You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

612 lines
22KB

  1. # coding: utf-8
  2. from __future__ import unicode_literals
  3. import functools
  4. import itertools
  5. import operator
  6. import re
  7. from .common import InfoExtractor
  8. from ..compat import (
  9. compat_HTTPError,
  10. compat_str,
  11. compat_urllib_request,
  12. )
  13. from .openload import PhantomJSwrapper
  14. from ..utils import (
  15. determine_ext,
  16. ExtractorError,
  17. int_or_none,
  18. NO_DEFAULT,
  19. orderedSet,
  20. remove_quotes,
  21. str_to_int,
  22. url_or_none,
  23. )
  24. class PornHubBaseIE(InfoExtractor):
  25. def _download_webpage_handle(self, *args, **kwargs):
  26. def dl(*args, **kwargs):
  27. return super(PornHubBaseIE, self)._download_webpage_handle(*args, **kwargs)
  28. webpage, urlh = dl(*args, **kwargs)
  29. if any(re.search(p, webpage) for p in (
  30. r'<body\b[^>]+\bonload=["\']go\(\)',
  31. r'document\.cookie\s*=\s*["\']RNKEY=',
  32. r'document\.location\.reload\(true\)')):
  33. url_or_request = args[0]
  34. url = (url_or_request.get_full_url()
  35. if isinstance(url_or_request, compat_urllib_request.Request)
  36. else url_or_request)
  37. phantom = PhantomJSwrapper(self, required_version='2.0')
  38. phantom.get(url, html=webpage)
  39. webpage, urlh = dl(*args, **kwargs)
  40. return webpage, urlh
  41. class PornHubIE(PornHubBaseIE):
  42. IE_DESC = 'PornHub and Thumbzilla'
  43. _VALID_URL = r'''(?x)
  44. https?://
  45. (?:
  46. (?:[^/]+\.)?(?P<host>pornhub(?:premium)?\.(?:com|net))/(?:(?:view_video\.php|video/show)\?viewkey=|embed/)|
  47. (?:www\.)?thumbzilla\.com/video/
  48. )
  49. (?P<id>[\da-z]+)
  50. '''
  51. _TESTS = [{
  52. 'url': 'http://www.pornhub.com/view_video.php?viewkey=648719015',
  53. 'md5': '1e19b41231a02eba417839222ac9d58e',
  54. 'info_dict': {
  55. 'id': '648719015',
  56. 'ext': 'mp4',
  57. 'title': 'Seductive Indian beauty strips down and fingers her pink pussy',
  58. 'uploader': 'Babes',
  59. 'upload_date': '20130628',
  60. 'duration': 361,
  61. 'view_count': int,
  62. 'like_count': int,
  63. 'dislike_count': int,
  64. 'comment_count': int,
  65. 'age_limit': 18,
  66. 'tags': list,
  67. 'categories': list,
  68. },
  69. }, {
  70. # non-ASCII title
  71. 'url': 'http://www.pornhub.com/view_video.php?viewkey=1331683002',
  72. 'info_dict': {
  73. 'id': '1331683002',
  74. 'ext': 'mp4',
  75. 'title': '重庆婷婷女王足交',
  76. 'uploader': 'Unknown',
  77. 'upload_date': '20150213',
  78. 'duration': 1753,
  79. 'view_count': int,
  80. 'like_count': int,
  81. 'dislike_count': int,
  82. 'comment_count': int,
  83. 'age_limit': 18,
  84. 'tags': list,
  85. 'categories': list,
  86. },
  87. 'params': {
  88. 'skip_download': True,
  89. },
  90. }, {
  91. # subtitles
  92. 'url': 'https://www.pornhub.com/view_video.php?viewkey=ph5af5fef7c2aa7',
  93. 'info_dict': {
  94. 'id': 'ph5af5fef7c2aa7',
  95. 'ext': 'mp4',
  96. 'title': 'BFFS - Cute Teen Girls Share Cock On the Floor',
  97. 'uploader': 'BFFs',
  98. 'duration': 622,
  99. 'view_count': int,
  100. 'like_count': int,
  101. 'dislike_count': int,
  102. 'comment_count': int,
  103. 'age_limit': 18,
  104. 'tags': list,
  105. 'categories': list,
  106. 'subtitles': {
  107. 'en': [{
  108. "ext": 'srt'
  109. }]
  110. },
  111. },
  112. 'params': {
  113. 'skip_download': True,
  114. },
  115. }, {
  116. 'url': 'http://www.pornhub.com/view_video.php?viewkey=ph557bbb6676d2d',
  117. 'only_matching': True,
  118. }, {
  119. # removed at the request of cam4.com
  120. 'url': 'http://fr.pornhub.com/view_video.php?viewkey=ph55ca2f9760862',
  121. 'only_matching': True,
  122. }, {
  123. # removed at the request of the copyright owner
  124. 'url': 'http://www.pornhub.com/view_video.php?viewkey=788152859',
  125. 'only_matching': True,
  126. }, {
  127. # removed by uploader
  128. 'url': 'http://www.pornhub.com/view_video.php?viewkey=ph572716d15a111',
  129. 'only_matching': True,
  130. }, {
  131. # private video
  132. 'url': 'http://www.pornhub.com/view_video.php?viewkey=ph56fd731fce6b7',
  133. 'only_matching': True,
  134. }, {
  135. 'url': 'https://www.thumbzilla.com/video/ph56c6114abd99a/horny-girlfriend-sex',
  136. 'only_matching': True,
  137. }, {
  138. 'url': 'http://www.pornhub.com/video/show?viewkey=648719015',
  139. 'only_matching': True,
  140. }, {
  141. 'url': 'https://www.pornhub.net/view_video.php?viewkey=203640933',
  142. 'only_matching': True,
  143. }, {
  144. 'url': 'https://www.pornhubpremium.com/view_video.php?viewkey=ph5e4acdae54a82',
  145. 'only_matching': True,
  146. }]
  147. @staticmethod
  148. def _extract_urls(webpage):
  149. return re.findall(
  150. r'<iframe[^>]+?src=["\'](?P<url>(?:https?:)?//(?:www\.)?pornhub\.(?:com|net)/embed/[\da-z]+)',
  151. webpage)
  152. def _extract_count(self, pattern, webpage, name):
  153. return str_to_int(self._search_regex(
  154. pattern, webpage, '%s count' % name, fatal=False))
  155. def _real_extract(self, url):
  156. mobj = re.match(self._VALID_URL, url)
  157. host = mobj.group('host') or 'pornhub.com'
  158. video_id = mobj.group('id')
  159. if 'premium' in host:
  160. if not self._downloader.params.get('cookiefile'):
  161. raise ExtractorError(
  162. 'PornHub Premium requires authentication.'
  163. ' You may want to use --cookies.',
  164. expected=True)
  165. self._set_cookie(host, 'age_verified', '1')
  166. def dl_webpage(platform):
  167. self._set_cookie(host, 'platform', platform)
  168. return self._download_webpage(
  169. 'https://www.%s/view_video.php?viewkey=%s' % (host, video_id),
  170. video_id, 'Downloading %s webpage' % platform)
  171. webpage = dl_webpage('pc')
  172. error_msg = self._html_search_regex(
  173. r'(?s)<div[^>]+class=(["\'])(?:(?!\1).)*\b(?:removed|userMessageSection)\b(?:(?!\1).)*\1[^>]*>(?P<error>.+?)</div>',
  174. webpage, 'error message', default=None, group='error')
  175. if error_msg:
  176. error_msg = re.sub(r'\s+', ' ', error_msg)
  177. raise ExtractorError(
  178. 'PornHub said: %s' % error_msg,
  179. expected=True, video_id=video_id)
  180. # video_title from flashvars contains whitespace instead of non-ASCII (see
  181. # http://www.pornhub.com/view_video.php?viewkey=1331683002), not relying
  182. # on that anymore.
  183. title = self._html_search_meta(
  184. 'twitter:title', webpage, default=None) or self._html_search_regex(
  185. (r'(?s)<h1[^>]+class=["\']title["\'][^>]*>(?P<title>.+?)</h1>',
  186. r'<div[^>]+data-video-title=(["\'])(?P<title>(?:(?!\1).)+)\1',
  187. r'shareTitle["\']\s*[=:]\s*(["\'])(?P<title>(?:(?!\1).)+)\1'),
  188. webpage, 'title', group='title')
  189. video_urls = []
  190. video_urls_set = set()
  191. subtitles = {}
  192. flashvars = self._parse_json(
  193. self._search_regex(
  194. r'var\s+flashvars_\d+\s*=\s*({.+?});', webpage, 'flashvars', default='{}'),
  195. video_id)
  196. if flashvars:
  197. subtitle_url = url_or_none(flashvars.get('closedCaptionsFile'))
  198. if subtitle_url:
  199. subtitles.setdefault('en', []).append({
  200. 'url': subtitle_url,
  201. 'ext': 'srt',
  202. })
  203. thumbnail = flashvars.get('image_url')
  204. duration = int_or_none(flashvars.get('video_duration'))
  205. media_definitions = flashvars.get('mediaDefinitions')
  206. if isinstance(media_definitions, list):
  207. for definition in media_definitions:
  208. if not isinstance(definition, dict):
  209. continue
  210. video_url = definition.get('videoUrl')
  211. if not video_url or not isinstance(video_url, compat_str):
  212. continue
  213. if video_url in video_urls_set:
  214. continue
  215. video_urls_set.add(video_url)
  216. video_urls.append(
  217. (video_url, int_or_none(definition.get('quality'))))
  218. else:
  219. thumbnail, duration = [None] * 2
  220. def extract_js_vars(webpage, pattern, default=NO_DEFAULT):
  221. assignments = self._search_regex(
  222. pattern, webpage, 'encoded url', default=default)
  223. if not assignments:
  224. return {}
  225. assignments = assignments.split(';')
  226. js_vars = {}
  227. def parse_js_value(inp):
  228. inp = re.sub(r'/\*(?:(?!\*/).)*?\*/', '', inp)
  229. if '+' in inp:
  230. inps = inp.split('+')
  231. return functools.reduce(
  232. operator.concat, map(parse_js_value, inps))
  233. inp = inp.strip()
  234. if inp in js_vars:
  235. return js_vars[inp]
  236. return remove_quotes(inp)
  237. for assn in assignments:
  238. assn = assn.strip()
  239. if not assn:
  240. continue
  241. assn = re.sub(r'var\s+', '', assn)
  242. vname, value = assn.split('=', 1)
  243. js_vars[vname] = parse_js_value(value)
  244. return js_vars
  245. def add_video_url(video_url):
  246. v_url = url_or_none(video_url)
  247. if not v_url:
  248. return
  249. if v_url in video_urls_set:
  250. return
  251. video_urls.append((v_url, None))
  252. video_urls_set.add(v_url)
  253. if not video_urls:
  254. FORMAT_PREFIXES = ('media', 'quality')
  255. js_vars = extract_js_vars(
  256. webpage, r'(var\s+(?:%s)_.+)' % '|'.join(FORMAT_PREFIXES),
  257. default=None)
  258. if js_vars:
  259. for key, format_url in js_vars.items():
  260. if any(key.startswith(p) for p in FORMAT_PREFIXES):
  261. add_video_url(format_url)
  262. if not video_urls and re.search(
  263. r'<[^>]+\bid=["\']lockedPlayer', webpage):
  264. raise ExtractorError(
  265. 'Video %s is locked' % video_id, expected=True)
  266. if not video_urls:
  267. js_vars = extract_js_vars(
  268. dl_webpage('tv'), r'(var.+?mediastring.+?)</script>')
  269. add_video_url(js_vars['mediastring'])
  270. for mobj in re.finditer(
  271. r'<a[^>]+\bclass=["\']downloadBtn\b[^>]+\bhref=(["\'])(?P<url>(?:(?!\1).)+)\1',
  272. webpage):
  273. video_url = mobj.group('url')
  274. if video_url not in video_urls_set:
  275. video_urls.append((video_url, None))
  276. video_urls_set.add(video_url)
  277. upload_date = None
  278. formats = []
  279. for video_url, height in video_urls:
  280. if not upload_date:
  281. upload_date = self._search_regex(
  282. r'/(\d{6}/\d{2})/', video_url, 'upload data', default=None)
  283. if upload_date:
  284. upload_date = upload_date.replace('/', '')
  285. ext = determine_ext(video_url)
  286. if ext == 'mpd':
  287. formats.extend(self._extract_mpd_formats(
  288. video_url, video_id, mpd_id='dash', fatal=False))
  289. continue
  290. elif ext == 'm3u8':
  291. formats.extend(self._extract_m3u8_formats(
  292. video_url, video_id, 'mp4', entry_protocol='m3u8_native',
  293. m3u8_id='hls', fatal=False))
  294. continue
  295. tbr = None
  296. mobj = re.search(r'(?P<height>\d+)[pP]?_(?P<tbr>\d+)[kK]', video_url)
  297. if mobj:
  298. if not height:
  299. height = int(mobj.group('height'))
  300. tbr = int(mobj.group('tbr'))
  301. formats.append({
  302. 'url': video_url,
  303. 'format_id': '%dp' % height if height else None,
  304. 'height': height,
  305. 'tbr': tbr,
  306. })
  307. self._sort_formats(formats)
  308. video_uploader = self._html_search_regex(
  309. r'(?s)From:&nbsp;.+?<(?:a\b[^>]+\bhref=["\']/(?:(?:user|channel)s|model|pornstar)/|span\b[^>]+\bclass=["\']username)[^>]+>(.+?)<',
  310. webpage, 'uploader', fatal=False)
  311. view_count = self._extract_count(
  312. r'<span class="count">([\d,\.]+)</span> [Vv]iews', webpage, 'view')
  313. like_count = self._extract_count(
  314. r'<span class="votesUp">([\d,\.]+)</span>', webpage, 'like')
  315. dislike_count = self._extract_count(
  316. r'<span class="votesDown">([\d,\.]+)</span>', webpage, 'dislike')
  317. comment_count = self._extract_count(
  318. r'All Comments\s*<span>\(([\d,.]+)\)', webpage, 'comment')
  319. def extract_list(meta_key):
  320. div = self._search_regex(
  321. r'(?s)<div[^>]+\bclass=["\'].*?\b%sWrapper[^>]*>(.+?)</div>'
  322. % meta_key, webpage, meta_key, default=None)
  323. if div:
  324. return re.findall(r'<a[^>]+\bhref=[^>]+>([^<]+)', div)
  325. return {
  326. 'id': video_id,
  327. 'uploader': video_uploader,
  328. 'upload_date': upload_date,
  329. 'title': title,
  330. 'thumbnail': thumbnail,
  331. 'duration': duration,
  332. 'view_count': view_count,
  333. 'like_count': like_count,
  334. 'dislike_count': dislike_count,
  335. 'comment_count': comment_count,
  336. 'formats': formats,
  337. 'age_limit': 18,
  338. 'tags': extract_list('tags'),
  339. 'categories': extract_list('categories'),
  340. 'subtitles': subtitles,
  341. }
  342. class PornHubPlaylistBaseIE(PornHubBaseIE):
  343. def _extract_entries(self, webpage, host):
  344. # Only process container div with main playlist content skipping
  345. # drop-down menu that uses similar pattern for videos (see
  346. # https://github.com/ytdl-org/youtube-dl/issues/11594).
  347. container = self._search_regex(
  348. r'(?s)(<div[^>]+class=["\']container.+)', webpage,
  349. 'container', default=webpage)
  350. return [
  351. self.url_result(
  352. 'http://www.%s/%s' % (host, video_url),
  353. PornHubIE.ie_key(), video_title=title)
  354. for video_url, title in orderedSet(re.findall(
  355. r'href="/?(view_video\.php\?.*\bviewkey=[\da-z]+[^"]*)"[^>]*\s+title="([^"]+)"',
  356. container))
  357. ]
  358. def _real_extract(self, url):
  359. mobj = re.match(self._VALID_URL, url)
  360. host = mobj.group('host')
  361. playlist_id = mobj.group('id')
  362. webpage = self._download_webpage(url, playlist_id)
  363. entries = self._extract_entries(webpage, host)
  364. playlist = self._parse_json(
  365. self._search_regex(
  366. r'(?:playlistObject|PLAYLIST_VIEW)\s*=\s*({.+?});', webpage,
  367. 'playlist', default='{}'),
  368. playlist_id, fatal=False)
  369. title = playlist.get('title') or self._search_regex(
  370. r'>Videos\s+in\s+(.+?)\s+[Pp]laylist<', webpage, 'title', fatal=False)
  371. return self.playlist_result(
  372. entries, playlist_id, title, playlist.get('description'))
  373. class PornHubUserIE(PornHubPlaylistBaseIE):
  374. _VALID_URL = r'(?P<url>https?://(?:[^/]+\.)?(?P<host>pornhub(?:premium)?\.(?:com|net))/(?:(?:user|channel)s|model|pornstar)/(?P<id>[^/?#&]+))(?:[?#&]|/(?!videos)|$)'
  375. _TESTS = [{
  376. 'url': 'https://www.pornhub.com/model/zoe_ph',
  377. 'playlist_mincount': 118,
  378. }, {
  379. 'url': 'https://www.pornhub.com/pornstar/liz-vicious',
  380. 'info_dict': {
  381. 'id': 'liz-vicious',
  382. },
  383. 'playlist_mincount': 118,
  384. }, {
  385. 'url': 'https://www.pornhub.com/users/russianveet69',
  386. 'only_matching': True,
  387. }, {
  388. 'url': 'https://www.pornhub.com/channels/povd',
  389. 'only_matching': True,
  390. }, {
  391. 'url': 'https://www.pornhub.com/model/zoe_ph?abc=1',
  392. 'only_matching': True,
  393. }]
  394. def _real_extract(self, url):
  395. mobj = re.match(self._VALID_URL, url)
  396. user_id = mobj.group('id')
  397. return self.url_result(
  398. '%s/videos' % mobj.group('url'), ie=PornHubPagedVideoListIE.ie_key(),
  399. video_id=user_id)
  400. class PornHubPagedPlaylistBaseIE(PornHubPlaylistBaseIE):
  401. @staticmethod
  402. def _has_more(webpage):
  403. return re.search(
  404. r'''(?x)
  405. <li[^>]+\bclass=["\']page_next|
  406. <link[^>]+\brel=["\']next|
  407. <button[^>]+\bid=["\']moreDataBtn
  408. ''', webpage) is not None
  409. def _real_extract(self, url):
  410. mobj = re.match(self._VALID_URL, url)
  411. host = mobj.group('host')
  412. item_id = mobj.group('id')
  413. page = int_or_none(self._search_regex(
  414. r'\bpage=(\d+)', url, 'page', default=None))
  415. entries = []
  416. for page_num in (page, ) if page is not None else itertools.count(1):
  417. try:
  418. webpage = self._download_webpage(
  419. url, item_id, 'Downloading page %d' % page_num,
  420. query={'page': page_num})
  421. except ExtractorError as e:
  422. if isinstance(e.cause, compat_HTTPError) and e.cause.code == 404:
  423. break
  424. raise
  425. page_entries = self._extract_entries(webpage, host)
  426. if not page_entries:
  427. break
  428. entries.extend(page_entries)
  429. if not self._has_more(webpage):
  430. break
  431. return self.playlist_result(orderedSet(entries), item_id)
  432. class PornHubPagedVideoListIE(PornHubPagedPlaylistBaseIE):
  433. _VALID_URL = r'https?://(?:[^/]+\.)?(?P<host>pornhub(?:premium)?\.(?:com|net))/(?P<id>(?:[^/]+/)*[^/?#&]+)'
  434. _TESTS = [{
  435. 'url': 'https://www.pornhub.com/model/zoe_ph/videos',
  436. 'only_matching': True,
  437. }, {
  438. 'url': 'http://www.pornhub.com/users/rushandlia/videos',
  439. 'only_matching': True,
  440. }, {
  441. 'url': 'https://www.pornhub.com/pornstar/jenny-blighe/videos',
  442. 'info_dict': {
  443. 'id': 'pornstar/jenny-blighe/videos',
  444. },
  445. 'playlist_mincount': 149,
  446. }, {
  447. 'url': 'https://www.pornhub.com/pornstar/jenny-blighe/videos?page=3',
  448. 'info_dict': {
  449. 'id': 'pornstar/jenny-blighe/videos',
  450. },
  451. 'playlist_mincount': 40,
  452. }, {
  453. # default sorting as Top Rated Videos
  454. 'url': 'https://www.pornhub.com/channels/povd/videos',
  455. 'info_dict': {
  456. 'id': 'channels/povd/videos',
  457. },
  458. 'playlist_mincount': 293,
  459. }, {
  460. # Top Rated Videos
  461. 'url': 'https://www.pornhub.com/channels/povd/videos?o=ra',
  462. 'only_matching': True,
  463. }, {
  464. # Most Recent Videos
  465. 'url': 'https://www.pornhub.com/channels/povd/videos?o=da',
  466. 'only_matching': True,
  467. }, {
  468. # Most Viewed Videos
  469. 'url': 'https://www.pornhub.com/channels/povd/videos?o=vi',
  470. 'only_matching': True,
  471. }, {
  472. 'url': 'http://www.pornhub.com/users/zoe_ph/videos/public',
  473. 'only_matching': True,
  474. }, {
  475. # Most Viewed Videos
  476. 'url': 'https://www.pornhub.com/pornstar/liz-vicious/videos?o=mv',
  477. 'only_matching': True,
  478. }, {
  479. # Top Rated Videos
  480. 'url': 'https://www.pornhub.com/pornstar/liz-vicious/videos?o=tr',
  481. 'only_matching': True,
  482. }, {
  483. # Longest Videos
  484. 'url': 'https://www.pornhub.com/pornstar/liz-vicious/videos?o=lg',
  485. 'only_matching': True,
  486. }, {
  487. # Newest Videos
  488. 'url': 'https://www.pornhub.com/pornstar/liz-vicious/videos?o=cm',
  489. 'only_matching': True,
  490. }, {
  491. 'url': 'https://www.pornhub.com/pornstar/liz-vicious/videos/paid',
  492. 'only_matching': True,
  493. }, {
  494. 'url': 'https://www.pornhub.com/pornstar/liz-vicious/videos/fanonly',
  495. 'only_matching': True,
  496. }, {
  497. 'url': 'https://www.pornhub.com/video',
  498. 'only_matching': True,
  499. }, {
  500. 'url': 'https://www.pornhub.com/video?page=3',
  501. 'only_matching': True,
  502. }, {
  503. 'url': 'https://www.pornhub.com/video/search?search=123',
  504. 'only_matching': True,
  505. }, {
  506. 'url': 'https://www.pornhub.com/categories/teen',
  507. 'only_matching': True,
  508. }, {
  509. 'url': 'https://www.pornhub.com/categories/teen?page=3',
  510. 'only_matching': True,
  511. }, {
  512. 'url': 'https://www.pornhub.com/hd',
  513. 'only_matching': True,
  514. }, {
  515. 'url': 'https://www.pornhub.com/hd?page=3',
  516. 'only_matching': True,
  517. }, {
  518. 'url': 'https://www.pornhub.com/described-video',
  519. 'only_matching': True,
  520. }, {
  521. 'url': 'https://www.pornhub.com/described-video?page=2',
  522. 'only_matching': True,
  523. }, {
  524. 'url': 'https://www.pornhub.com/video/incategories/60fps-1/hd-porn',
  525. 'only_matching': True,
  526. }, {
  527. 'url': 'https://www.pornhub.com/playlist/44121572',
  528. 'info_dict': {
  529. 'id': 'playlist/44121572',
  530. },
  531. 'playlist_mincount': 132,
  532. }, {
  533. 'url': 'https://www.pornhub.com/playlist/4667351',
  534. 'only_matching': True,
  535. }, {
  536. 'url': 'https://de.pornhub.com/playlist/4667351',
  537. 'only_matching': True,
  538. }]
  539. @classmethod
  540. def suitable(cls, url):
  541. return (False
  542. if PornHubIE.suitable(url) or PornHubUserIE.suitable(url) or PornHubUserVideosUploadIE.suitable(url)
  543. else super(PornHubPagedVideoListIE, cls).suitable(url))
  544. class PornHubUserVideosUploadIE(PornHubPagedPlaylistBaseIE):
  545. _VALID_URL = r'(?P<url>https?://(?:[^/]+\.)?(?P<host>pornhub(?:premium)?\.(?:com|net))/(?:(?:user|channel)s|model|pornstar)/(?P<id>[^/]+)/videos/upload)'
  546. _TESTS = [{
  547. 'url': 'https://www.pornhub.com/pornstar/jenny-blighe/videos/upload',
  548. 'info_dict': {
  549. 'id': 'jenny-blighe',
  550. },
  551. 'playlist_mincount': 129,
  552. }, {
  553. 'url': 'https://www.pornhub.com/model/zoe_ph/videos/upload',
  554. 'only_matching': True,
  555. }]