pornhub.py 23 KB

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