logo

youtube-dl

[mirror] Download/Watch videos from video hostersgit clone https://hacktivis.me/git/mirror/youtube-dl.git

pornhub.py (28644B)


  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. update_url_query,
  24. urlencode_postdata,
  25. url_or_none,
  26. )
  27. class PornHubBaseIE(InfoExtractor):
  28. _NETRC_MACHINE = 'pornhub'
  29. _PORNHUB_HOST_RE = r'(?:(?P<host>pornhub(?:premium)?\.(?:com|net|org))|pornhubthbh7ap3u\.onion)'
  30. def _download_webpage_handle(self, *args, **kwargs):
  31. def dl(*args, **kwargs):
  32. return super(PornHubBaseIE, self)._download_webpage_handle(*args, **kwargs)
  33. ret = dl(*args, **kwargs)
  34. if not ret:
  35. return ret
  36. webpage, urlh = ret
  37. if any(re.search(p, webpage) for p in (
  38. r'<body\b[^>]+\bonload=["\']go\(\)',
  39. r'document\.cookie\s*=\s*["\']RNKEY=',
  40. r'document\.location\.reload\(true\)')):
  41. url_or_request = args[0]
  42. url = (url_or_request.get_full_url()
  43. if isinstance(url_or_request, compat_urllib_request.Request)
  44. else url_or_request)
  45. phantom = PhantomJSwrapper(self, required_version='2.0')
  46. phantom.get(url, html=webpage)
  47. webpage, urlh = dl(*args, **kwargs)
  48. return webpage, urlh
  49. def _real_initialize(self):
  50. self._logged_in = False
  51. def _login(self, host):
  52. if self._logged_in:
  53. return
  54. site = host.split('.')[0]
  55. # Both sites pornhub and pornhubpremium have separate accounts
  56. # so there should be an option to provide credentials for both.
  57. # At the same time some videos are available under the same video id
  58. # on both sites so that we have to identify them as the same video.
  59. # For that purpose we have to keep both in the same extractor
  60. # but under different netrc machines.
  61. username, password = self._get_login_info(netrc_machine=site)
  62. if username is None:
  63. return
  64. login_url = 'https://www.%s/%slogin' % (host, 'premium/' if 'premium' in host else '')
  65. login_page = self._download_webpage(
  66. login_url, None, 'Downloading %s login page' % site)
  67. def is_logged(webpage):
  68. return any(re.search(p, webpage) for p in (
  69. r'class=["\']signOut',
  70. r'>Sign\s+[Oo]ut\s*<'))
  71. if is_logged(login_page):
  72. self._logged_in = True
  73. return
  74. login_form = self._hidden_inputs(login_page)
  75. login_form.update({
  76. 'username': username,
  77. 'password': password,
  78. })
  79. response = self._download_json(
  80. 'https://www.%s/front/authenticate' % host, None,
  81. 'Logging in to %s' % site,
  82. data=urlencode_postdata(login_form),
  83. headers={
  84. 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
  85. 'Referer': login_url,
  86. 'X-Requested-With': 'XMLHttpRequest',
  87. })
  88. if response.get('success') == '1':
  89. self._logged_in = True
  90. return
  91. message = response.get('message')
  92. if message is not None:
  93. raise ExtractorError(
  94. 'Unable to login: %s' % message, expected=True)
  95. raise ExtractorError('Unable to log in')
  96. class PornHubIE(PornHubBaseIE):
  97. IE_DESC = 'PornHub and Thumbzilla'
  98. _VALID_URL = r'''(?x)
  99. https?://
  100. (?:
  101. (?:[^/]+\.)?
  102. %s
  103. /(?:(?:view_video\.php|video/show)\?viewkey=|embed/)|
  104. (?:www\.)?thumbzilla\.com/video/
  105. )
  106. (?P<id>[\da-z]+)
  107. ''' % PornHubBaseIE._PORNHUB_HOST_RE
  108. _TESTS = [{
  109. 'url': 'http://www.pornhub.com/view_video.php?viewkey=648719015',
  110. 'md5': 'a6391306d050e4547f62b3f485dd9ba9',
  111. 'info_dict': {
  112. 'id': '648719015',
  113. 'ext': 'mp4',
  114. 'title': 'Seductive Indian beauty strips down and fingers her pink pussy',
  115. 'uploader': 'Babes',
  116. 'upload_date': '20130628',
  117. 'timestamp': 1372447216,
  118. 'duration': 361,
  119. 'view_count': int,
  120. 'like_count': int,
  121. 'dislike_count': int,
  122. 'comment_count': int,
  123. 'age_limit': 18,
  124. 'tags': list,
  125. 'categories': list,
  126. },
  127. }, {
  128. # non-ASCII title
  129. 'url': 'http://www.pornhub.com/view_video.php?viewkey=1331683002',
  130. 'info_dict': {
  131. 'id': '1331683002',
  132. 'ext': 'mp4',
  133. 'title': '重庆婷婷女王足交',
  134. 'upload_date': '20150213',
  135. 'timestamp': 1423804862,
  136. 'duration': 1753,
  137. 'view_count': int,
  138. 'like_count': int,
  139. 'dislike_count': int,
  140. 'comment_count': int,
  141. 'age_limit': 18,
  142. 'tags': list,
  143. 'categories': list,
  144. },
  145. 'params': {
  146. 'skip_download': True,
  147. },
  148. 'skip': 'Video has been flagged for verification in accordance with our trust and safety policy',
  149. }, {
  150. # subtitles
  151. 'url': 'https://www.pornhub.com/view_video.php?viewkey=ph5af5fef7c2aa7',
  152. 'info_dict': {
  153. 'id': 'ph5af5fef7c2aa7',
  154. 'ext': 'mp4',
  155. 'title': 'BFFS - Cute Teen Girls Share Cock On the Floor',
  156. 'uploader': 'BFFs',
  157. 'duration': 622,
  158. 'view_count': int,
  159. 'like_count': int,
  160. 'dislike_count': int,
  161. 'comment_count': int,
  162. 'age_limit': 18,
  163. 'tags': list,
  164. 'categories': list,
  165. 'subtitles': {
  166. 'en': [{
  167. "ext": 'srt'
  168. }]
  169. },
  170. },
  171. 'params': {
  172. 'skip_download': True,
  173. },
  174. 'skip': 'This video has been disabled',
  175. }, {
  176. 'url': 'http://www.pornhub.com/view_video.php?viewkey=ph557bbb6676d2d',
  177. 'only_matching': True,
  178. }, {
  179. # removed at the request of cam4.com
  180. 'url': 'http://fr.pornhub.com/view_video.php?viewkey=ph55ca2f9760862',
  181. 'only_matching': True,
  182. }, {
  183. # removed at the request of the copyright owner
  184. 'url': 'http://www.pornhub.com/view_video.php?viewkey=788152859',
  185. 'only_matching': True,
  186. }, {
  187. # removed by uploader
  188. 'url': 'http://www.pornhub.com/view_video.php?viewkey=ph572716d15a111',
  189. 'only_matching': True,
  190. }, {
  191. # private video
  192. 'url': 'http://www.pornhub.com/view_video.php?viewkey=ph56fd731fce6b7',
  193. 'only_matching': True,
  194. }, {
  195. 'url': 'https://www.thumbzilla.com/video/ph56c6114abd99a/horny-girlfriend-sex',
  196. 'only_matching': True,
  197. }, {
  198. 'url': 'http://www.pornhub.com/video/show?viewkey=648719015',
  199. 'only_matching': True,
  200. }, {
  201. 'url': 'https://www.pornhub.net/view_video.php?viewkey=203640933',
  202. 'only_matching': True,
  203. }, {
  204. 'url': 'https://www.pornhub.org/view_video.php?viewkey=203640933',
  205. 'only_matching': True,
  206. }, {
  207. 'url': 'https://www.pornhubpremium.com/view_video.php?viewkey=ph5e4acdae54a82',
  208. 'only_matching': True,
  209. }, {
  210. # Some videos are available with the same id on both premium
  211. # and non-premium sites (e.g. this and the following test)
  212. 'url': 'https://www.pornhub.com/view_video.php?viewkey=ph5f75b0f4b18e3',
  213. 'only_matching': True,
  214. }, {
  215. 'url': 'https://www.pornhubpremium.com/view_video.php?viewkey=ph5f75b0f4b18e3',
  216. 'only_matching': True,
  217. }, {
  218. # geo restricted
  219. 'url': 'https://www.pornhub.com/view_video.php?viewkey=ph5a9813bfa7156',
  220. 'only_matching': True,
  221. }, {
  222. 'url': 'http://pornhubthbh7ap3u.onion/view_video.php?viewkey=ph5a9813bfa7156',
  223. 'only_matching': True,
  224. }]
  225. @staticmethod
  226. def _extract_urls(webpage):
  227. return re.findall(
  228. r'<iframe[^>]+?src=["\'](?P<url>(?:https?:)?//(?:www\.)?pornhub(?:premium)?\.(?:com|net|org)/embed/[\da-z]+)',
  229. webpage)
  230. def _extract_count(self, pattern, webpage, name):
  231. return str_to_int(self._search_regex(
  232. pattern, webpage, '%s count' % name, fatal=False))
  233. def _real_extract(self, url):
  234. mobj = re.match(self._VALID_URL, url)
  235. host = mobj.group('host') or 'pornhub.com'
  236. video_id = mobj.group('id')
  237. self._login(host)
  238. self._set_cookie(host, 'age_verified', '1')
  239. def dl_webpage(platform):
  240. self._set_cookie(host, 'platform', platform)
  241. return self._download_webpage(
  242. 'https://www.%s/view_video.php?viewkey=%s' % (host, video_id),
  243. video_id, 'Downloading %s webpage' % platform)
  244. webpage = dl_webpage('pc')
  245. error_msg = self._html_search_regex(
  246. (r'(?s)<div[^>]+class=(["\'])(?:(?!\1).)*\b(?:removed|userMessageSection)\b(?:(?!\1).)*\1[^>]*>(?P<error>.+?)</div>',
  247. r'(?s)<section[^>]+class=["\']noVideo["\'][^>]*>(?P<error>.+?)</section>'),
  248. webpage, 'error message', default=None, group='error')
  249. if error_msg:
  250. error_msg = re.sub(r'\s+', ' ', error_msg)
  251. raise ExtractorError(
  252. 'PornHub said: %s' % error_msg,
  253. expected=True, video_id=video_id)
  254. if any(re.search(p, webpage) for p in (
  255. r'class=["\']geoBlocked["\']',
  256. r'>\s*This content is unavailable in your country')):
  257. self.raise_geo_restricted()
  258. # video_title from flashvars contains whitespace instead of non-ASCII (see
  259. # http://www.pornhub.com/view_video.php?viewkey=1331683002), not relying
  260. # on that anymore.
  261. title = self._html_search_meta(
  262. 'twitter:title', webpage, default=None) or self._html_search_regex(
  263. (r'(?s)<h1[^>]+class=["\']title["\'][^>]*>(?P<title>.+?)</h1>',
  264. r'<div[^>]+data-video-title=(["\'])(?P<title>(?:(?!\1).)+)\1',
  265. r'shareTitle["\']\s*[=:]\s*(["\'])(?P<title>(?:(?!\1).)+)\1'),
  266. webpage, 'title', group='title')
  267. video_urls = []
  268. video_urls_set = set()
  269. subtitles = {}
  270. flashvars = self._parse_json(
  271. self._search_regex(
  272. r'var\s+flashvars_\d+\s*=\s*({.+?});', webpage, 'flashvars', default='{}'),
  273. video_id)
  274. if flashvars:
  275. subtitle_url = url_or_none(flashvars.get('closedCaptionsFile'))
  276. if subtitle_url:
  277. subtitles.setdefault('en', []).append({
  278. 'url': subtitle_url,
  279. 'ext': 'srt',
  280. })
  281. thumbnail = flashvars.get('image_url')
  282. duration = int_or_none(flashvars.get('video_duration'))
  283. media_definitions = flashvars.get('mediaDefinitions')
  284. if isinstance(media_definitions, list):
  285. for definition in media_definitions:
  286. if not isinstance(definition, dict):
  287. continue
  288. video_url = definition.get('videoUrl')
  289. if not video_url or not isinstance(video_url, compat_str):
  290. continue
  291. if video_url in video_urls_set:
  292. continue
  293. video_urls_set.add(video_url)
  294. video_urls.append(
  295. (video_url, int_or_none(definition.get('quality'))))
  296. else:
  297. thumbnail, duration = [None] * 2
  298. def extract_js_vars(webpage, pattern, default=NO_DEFAULT):
  299. assignments = self._search_regex(
  300. pattern, webpage, 'encoded url', default=default)
  301. if not assignments:
  302. return {}
  303. assignments = assignments.split(';')
  304. js_vars = {}
  305. def parse_js_value(inp):
  306. inp = re.sub(r'/\*(?:(?!\*/).)*?\*/', '', inp)
  307. if '+' in inp:
  308. inps = inp.split('+')
  309. return functools.reduce(
  310. operator.concat, map(parse_js_value, inps))
  311. inp = inp.strip()
  312. if inp in js_vars:
  313. return js_vars[inp]
  314. return remove_quotes(inp)
  315. for assn in assignments:
  316. assn = assn.strip()
  317. if not assn:
  318. continue
  319. assn = re.sub(r'var\s+', '', assn)
  320. vname, value = assn.split('=', 1)
  321. js_vars[vname] = parse_js_value(value)
  322. return js_vars
  323. def add_video_url(video_url):
  324. v_url = url_or_none(video_url)
  325. if not v_url:
  326. return
  327. if v_url in video_urls_set:
  328. return
  329. video_urls.append((v_url, None))
  330. video_urls_set.add(v_url)
  331. def parse_quality_items(quality_items):
  332. q_items = self._parse_json(quality_items, video_id, fatal=False)
  333. if not isinstance(q_items, list):
  334. return
  335. for item in q_items:
  336. if isinstance(item, dict):
  337. add_video_url(item.get('url'))
  338. if not video_urls:
  339. FORMAT_PREFIXES = ('media', 'quality', 'qualityItems')
  340. js_vars = extract_js_vars(
  341. webpage, r'(var\s+(?:%s)_.+)' % '|'.join(FORMAT_PREFIXES),
  342. default=None)
  343. if js_vars:
  344. for key, format_url in js_vars.items():
  345. if key.startswith(FORMAT_PREFIXES[-1]):
  346. parse_quality_items(format_url)
  347. elif any(key.startswith(p) for p in FORMAT_PREFIXES[:2]):
  348. add_video_url(format_url)
  349. if not video_urls and re.search(
  350. r'<[^>]+\bid=["\']lockedPlayer', webpage):
  351. raise ExtractorError(
  352. 'Video %s is locked' % video_id, expected=True)
  353. if not video_urls:
  354. js_vars = extract_js_vars(
  355. dl_webpage('tv'), r'(var.+?mediastring.+?)</script>')
  356. add_video_url(js_vars['mediastring'])
  357. for mobj in re.finditer(
  358. r'<a[^>]+\bclass=["\']downloadBtn\b[^>]+\bhref=(["\'])(?P<url>(?:(?!\1).)+)\1',
  359. webpage):
  360. video_url = mobj.group('url')
  361. if video_url not in video_urls_set:
  362. video_urls.append((video_url, None))
  363. video_urls_set.add(video_url)
  364. upload_date = None
  365. formats = []
  366. def add_format(format_url, height=None):
  367. ext = determine_ext(format_url)
  368. if ext == 'mpd':
  369. formats.extend(self._extract_mpd_formats(
  370. format_url, video_id, mpd_id='dash', fatal=False))
  371. return
  372. if ext == 'm3u8':
  373. formats.extend(self._extract_m3u8_formats(
  374. format_url, video_id, 'mp4', entry_protocol='m3u8_native',
  375. m3u8_id='hls', fatal=False))
  376. return
  377. if not height:
  378. height = int_or_none(self._search_regex(
  379. r'(?P<height>\d+)[pP]?_\d+[kK]', format_url, 'height',
  380. default=None))
  381. formats.append({
  382. 'url': format_url,
  383. 'format_id': '%dp' % height if height else None,
  384. 'height': height,
  385. })
  386. for video_url, height in video_urls:
  387. if not upload_date:
  388. upload_date = self._search_regex(
  389. r'/(\d{6}/\d{2})/', video_url, 'upload data', default=None)
  390. if upload_date:
  391. upload_date = upload_date.replace('/', '')
  392. if '/video/get_media' in video_url:
  393. medias = self._download_json(video_url, video_id, fatal=False)
  394. if isinstance(medias, list):
  395. for media in medias:
  396. if not isinstance(media, dict):
  397. continue
  398. video_url = url_or_none(media.get('videoUrl'))
  399. if not video_url:
  400. continue
  401. height = int_or_none(media.get('quality'))
  402. add_format(video_url, height)
  403. continue
  404. add_format(video_url)
  405. self._sort_formats(
  406. formats, field_preference=('height', 'width', 'fps', 'format_id'))
  407. video_uploader = self._html_search_regex(
  408. r'(?s)From:&nbsp;.+?<(?:a\b[^>]+\bhref=["\']/(?:(?:user|channel)s|model|pornstar)/|span\b[^>]+\bclass=["\']username)[^>]+>(.+?)<',
  409. webpage, 'uploader', default=None)
  410. def extract_vote_count(kind, name):
  411. return self._extract_count(
  412. (r'<span[^>]+\bclass="votes%s"[^>]*>([\d,\.]+)</span>' % kind,
  413. r'<span[^>]+\bclass=["\']votes%s["\'][^>]*\bdata-rating=["\'](\d+)' % kind),
  414. webpage, name)
  415. view_count = self._extract_count(
  416. r'<span class="count">([\d,\.]+)</span> [Vv]iews', webpage, 'view')
  417. like_count = extract_vote_count('Up', 'like')
  418. dislike_count = extract_vote_count('Down', 'dislike')
  419. comment_count = self._extract_count(
  420. r'All Comments\s*<span>\(([\d,.]+)\)', webpage, 'comment')
  421. def extract_list(meta_key):
  422. div = self._search_regex(
  423. r'(?s)<div[^>]+\bclass=["\'].*?\b%sWrapper[^>]*>(.+?)</div>'
  424. % meta_key, webpage, meta_key, default=None)
  425. if div:
  426. return re.findall(r'<a[^>]+\bhref=[^>]+>([^<]+)', div)
  427. info = self._search_json_ld(webpage, video_id, default={})
  428. # description provided in JSON-LD is irrelevant
  429. info['description'] = None
  430. return merge_dicts({
  431. 'id': video_id,
  432. 'uploader': video_uploader,
  433. 'upload_date': upload_date,
  434. 'title': title,
  435. 'thumbnail': thumbnail,
  436. 'duration': duration,
  437. 'view_count': view_count,
  438. 'like_count': like_count,
  439. 'dislike_count': dislike_count,
  440. 'comment_count': comment_count,
  441. 'formats': formats,
  442. 'age_limit': 18,
  443. 'tags': extract_list('tags'),
  444. 'categories': extract_list('categories'),
  445. 'subtitles': subtitles,
  446. }, info)
  447. class PornHubPlaylistBaseIE(PornHubBaseIE):
  448. def _extract_page(self, url):
  449. return int_or_none(self._search_regex(
  450. r'\bpage=(\d+)', url, 'page', default=None))
  451. def _extract_entries(self, webpage, host):
  452. # Only process container div with main playlist content skipping
  453. # drop-down menu that uses similar pattern for videos (see
  454. # https://github.com/ytdl-org/youtube-dl/issues/11594).
  455. container = self._search_regex(
  456. r'(?s)(<div[^>]+class=["\']container.+)', webpage,
  457. 'container', default=webpage)
  458. return [
  459. self.url_result(
  460. 'http://www.%s/%s' % (host, video_url),
  461. PornHubIE.ie_key(), video_title=title)
  462. for video_url, title in orderedSet(re.findall(
  463. r'href="/?(view_video\.php\?.*\bviewkey=[\da-z]+[^"]*)"[^>]*\s+title="([^"]+)"',
  464. container))
  465. ]
  466. class PornHubUserIE(PornHubPlaylistBaseIE):
  467. _VALID_URL = r'(?P<url>https?://(?:[^/]+\.)?%s/(?:(?:user|channel)s|model|pornstar)/(?P<id>[^/?#&]+))(?:[?#&]|/(?!videos)|$)' % PornHubBaseIE._PORNHUB_HOST_RE
  468. _TESTS = [{
  469. 'url': 'https://www.pornhub.com/model/zoe_ph',
  470. 'playlist_mincount': 118,
  471. }, {
  472. 'url': 'https://www.pornhub.com/pornstar/liz-vicious',
  473. 'info_dict': {
  474. 'id': 'liz-vicious',
  475. },
  476. 'playlist_mincount': 118,
  477. }, {
  478. 'url': 'https://www.pornhub.com/users/russianveet69',
  479. 'only_matching': True,
  480. }, {
  481. 'url': 'https://www.pornhub.com/channels/povd',
  482. 'only_matching': True,
  483. }, {
  484. 'url': 'https://www.pornhub.com/model/zoe_ph?abc=1',
  485. 'only_matching': True,
  486. }, {
  487. # Unavailable via /videos page, but available with direct pagination
  488. # on pornstar page (see [1]), requires premium
  489. # 1. https://github.com/ytdl-org/youtube-dl/issues/27853
  490. 'url': 'https://www.pornhubpremium.com/pornstar/sienna-west',
  491. 'only_matching': True,
  492. }, {
  493. # Same as before, multi page
  494. 'url': 'https://www.pornhubpremium.com/pornstar/lily-labeau',
  495. 'only_matching': True,
  496. }, {
  497. 'url': 'https://pornhubthbh7ap3u.onion/model/zoe_ph',
  498. 'only_matching': True,
  499. }]
  500. def _real_extract(self, url):
  501. mobj = re.match(self._VALID_URL, url)
  502. user_id = mobj.group('id')
  503. videos_url = '%s/videos' % mobj.group('url')
  504. page = self._extract_page(url)
  505. if page:
  506. videos_url = update_url_query(videos_url, {'page': page})
  507. return self.url_result(
  508. videos_url, ie=PornHubPagedVideoListIE.ie_key(), video_id=user_id)
  509. class PornHubPagedPlaylistBaseIE(PornHubPlaylistBaseIE):
  510. @staticmethod
  511. def _has_more(webpage):
  512. return re.search(
  513. r'''(?x)
  514. <li[^>]+\bclass=["\']page_next|
  515. <link[^>]+\brel=["\']next|
  516. <button[^>]+\bid=["\']moreDataBtn
  517. ''', webpage) is not None
  518. def _entries(self, url, host, item_id):
  519. page = self._extract_page(url)
  520. VIDEOS = '/videos'
  521. def download_page(base_url, num, fallback=False):
  522. note = 'Downloading page %d%s' % (num, ' (switch to fallback)' if fallback else '')
  523. return self._download_webpage(
  524. base_url, item_id, note, query={'page': num})
  525. def is_404(e):
  526. return isinstance(e.cause, compat_HTTPError) and e.cause.code == 404
  527. base_url = url
  528. has_page = page is not None
  529. first_page = page if has_page else 1
  530. for page_num in (first_page, ) if has_page else itertools.count(first_page):
  531. try:
  532. try:
  533. webpage = download_page(base_url, page_num)
  534. except ExtractorError as e:
  535. # Some sources may not be available via /videos page,
  536. # trying to fallback to main page pagination (see [1])
  537. # 1. https://github.com/ytdl-org/youtube-dl/issues/27853
  538. if is_404(e) and page_num == first_page and VIDEOS in base_url:
  539. base_url = base_url.replace(VIDEOS, '')
  540. webpage = download_page(base_url, page_num, fallback=True)
  541. else:
  542. raise
  543. except ExtractorError as e:
  544. if is_404(e) and page_num != first_page:
  545. break
  546. raise
  547. page_entries = self._extract_entries(webpage, host)
  548. if not page_entries:
  549. break
  550. for e in page_entries:
  551. yield e
  552. if not self._has_more(webpage):
  553. break
  554. def _real_extract(self, url):
  555. mobj = re.match(self._VALID_URL, url)
  556. host = mobj.group('host')
  557. item_id = mobj.group('id')
  558. self._login(host)
  559. return self.playlist_result(self._entries(url, host, item_id), item_id)
  560. class PornHubPagedVideoListIE(PornHubPagedPlaylistBaseIE):
  561. _VALID_URL = r'https?://(?:[^/]+\.)?%s/(?P<id>(?:[^/]+/)*[^/?#&]+)' % PornHubBaseIE._PORNHUB_HOST_RE
  562. _TESTS = [{
  563. 'url': 'https://www.pornhub.com/model/zoe_ph/videos',
  564. 'only_matching': True,
  565. }, {
  566. 'url': 'http://www.pornhub.com/users/rushandlia/videos',
  567. 'only_matching': True,
  568. }, {
  569. 'url': 'https://www.pornhub.com/pornstar/jenny-blighe/videos',
  570. 'info_dict': {
  571. 'id': 'pornstar/jenny-blighe/videos',
  572. },
  573. 'playlist_mincount': 149,
  574. }, {
  575. 'url': 'https://www.pornhub.com/pornstar/jenny-blighe/videos?page=3',
  576. 'info_dict': {
  577. 'id': 'pornstar/jenny-blighe/videos',
  578. },
  579. 'playlist_mincount': 40,
  580. }, {
  581. # default sorting as Top Rated Videos
  582. 'url': 'https://www.pornhub.com/channels/povd/videos',
  583. 'info_dict': {
  584. 'id': 'channels/povd/videos',
  585. },
  586. 'playlist_mincount': 293,
  587. }, {
  588. # Top Rated Videos
  589. 'url': 'https://www.pornhub.com/channels/povd/videos?o=ra',
  590. 'only_matching': True,
  591. }, {
  592. # Most Recent Videos
  593. 'url': 'https://www.pornhub.com/channels/povd/videos?o=da',
  594. 'only_matching': True,
  595. }, {
  596. # Most Viewed Videos
  597. 'url': 'https://www.pornhub.com/channels/povd/videos?o=vi',
  598. 'only_matching': True,
  599. }, {
  600. 'url': 'http://www.pornhub.com/users/zoe_ph/videos/public',
  601. 'only_matching': True,
  602. }, {
  603. # Most Viewed Videos
  604. 'url': 'https://www.pornhub.com/pornstar/liz-vicious/videos?o=mv',
  605. 'only_matching': True,
  606. }, {
  607. # Top Rated Videos
  608. 'url': 'https://www.pornhub.com/pornstar/liz-vicious/videos?o=tr',
  609. 'only_matching': True,
  610. }, {
  611. # Longest Videos
  612. 'url': 'https://www.pornhub.com/pornstar/liz-vicious/videos?o=lg',
  613. 'only_matching': True,
  614. }, {
  615. # Newest Videos
  616. 'url': 'https://www.pornhub.com/pornstar/liz-vicious/videos?o=cm',
  617. 'only_matching': True,
  618. }, {
  619. 'url': 'https://www.pornhub.com/pornstar/liz-vicious/videos/paid',
  620. 'only_matching': True,
  621. }, {
  622. 'url': 'https://www.pornhub.com/pornstar/liz-vicious/videos/fanonly',
  623. 'only_matching': True,
  624. }, {
  625. 'url': 'https://www.pornhub.com/video',
  626. 'only_matching': True,
  627. }, {
  628. 'url': 'https://www.pornhub.com/video?page=3',
  629. 'only_matching': True,
  630. }, {
  631. 'url': 'https://www.pornhub.com/video/search?search=123',
  632. 'only_matching': True,
  633. }, {
  634. 'url': 'https://www.pornhub.com/categories/teen',
  635. 'only_matching': True,
  636. }, {
  637. 'url': 'https://www.pornhub.com/categories/teen?page=3',
  638. 'only_matching': True,
  639. }, {
  640. 'url': 'https://www.pornhub.com/hd',
  641. 'only_matching': True,
  642. }, {
  643. 'url': 'https://www.pornhub.com/hd?page=3',
  644. 'only_matching': True,
  645. }, {
  646. 'url': 'https://www.pornhub.com/described-video',
  647. 'only_matching': True,
  648. }, {
  649. 'url': 'https://www.pornhub.com/described-video?page=2',
  650. 'only_matching': True,
  651. }, {
  652. 'url': 'https://www.pornhub.com/video/incategories/60fps-1/hd-porn',
  653. 'only_matching': True,
  654. }, {
  655. 'url': 'https://www.pornhub.com/playlist/44121572',
  656. 'info_dict': {
  657. 'id': 'playlist/44121572',
  658. },
  659. 'playlist_mincount': 132,
  660. }, {
  661. 'url': 'https://www.pornhub.com/playlist/4667351',
  662. 'only_matching': True,
  663. }, {
  664. 'url': 'https://de.pornhub.com/playlist/4667351',
  665. 'only_matching': True,
  666. }, {
  667. 'url': 'https://pornhubthbh7ap3u.onion/model/zoe_ph/videos',
  668. 'only_matching': True,
  669. }]
  670. @classmethod
  671. def suitable(cls, url):
  672. return (False
  673. if PornHubIE.suitable(url) or PornHubUserIE.suitable(url) or PornHubUserVideosUploadIE.suitable(url)
  674. else super(PornHubPagedVideoListIE, cls).suitable(url))
  675. class PornHubUserVideosUploadIE(PornHubPagedPlaylistBaseIE):
  676. _VALID_URL = r'(?P<url>https?://(?:[^/]+\.)?%s/(?:(?:user|channel)s|model|pornstar)/(?P<id>[^/]+)/videos/upload)' % PornHubBaseIE._PORNHUB_HOST_RE
  677. _TESTS = [{
  678. 'url': 'https://www.pornhub.com/pornstar/jenny-blighe/videos/upload',
  679. 'info_dict': {
  680. 'id': 'jenny-blighe',
  681. },
  682. 'playlist_mincount': 129,
  683. }, {
  684. 'url': 'https://www.pornhub.com/model/zoe_ph/videos/upload',
  685. 'only_matching': True,
  686. }, {
  687. 'url': 'http://pornhubthbh7ap3u.onion/pornstar/jenny-blighe/videos/upload',
  688. 'only_matching': True,
  689. }]