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