pornhub.py 22 KB

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