7 from .common
import InfoExtractor
8 from .openload
import PhantomJSwrapper
9 from ..networking
import Request
10 from ..networking
.exceptions
import HTTPError
29 class PornHubBaseIE(InfoExtractor
):
30 _NETRC_MACHINE
= 'pornhub'
31 _PORNHUB_HOST_RE
= r
'(?:(?P<host>pornhub(?:premium)?\.(?:com|net|org))|pornhubvybmsymdol4iibwgwtkpwmeyd6luq2gxajgjzfjvotyt5zhyd\.onion)'
33 def _download_webpage_handle(self
, *args
, **kwargs
):
34 def dl(*args
, **kwargs
):
35 return super(PornHubBaseIE
, self
)._download
_webpage
_handle
(*args
, **kwargs
)
37 ret
= dl(*args
, **kwargs
)
44 if any(re
.search(p
, webpage
) for p
in (
45 r
'<body\b[^>]+\bonload=["\']go\
(\
)',
46 r'document\
.cookie\s
*=\s
*["\']RNKEY=',
47 r'document\.location\.reload\(true\)')):
48 url_or_request = args[0]
49 url = (url_or_request.url
50 if isinstance(url_or_request, Request)
52 phantom = PhantomJSwrapper(self, required_version='2.0')
53 phantom.get(url, html=webpage)
54 webpage, urlh = dl(*args, **kwargs)
58 def _real_initialize(self):
59 self._logged_in = False
61 def _set_age_cookies(self, host):
62 self._set_cookie(host, 'age_verified', '1')
63 self._set_cookie(host, 'accessAgeDisclaimerPH', '1')
64 self._set_cookie(host, 'accessAgeDisclaimerUK', '1')
65 self._set_cookie(host, 'accessPH', '1')
67 def _login(self, host):
71 site = host.split('.')[0]
73 # Both sites pornhub and pornhubpremium have separate accounts
74 # so there should be an option to provide credentials for both.
75 # At the same time some videos are available under the same video id
76 # on both sites so that we have to identify them as the same video.
77 # For that purpose we have to keep both in the same extractor
78 # but under different netrc machines.
79 username, password = self._get_login_info(netrc_machine=site)
83 login_url = 'https://www.{}/{}login'.format(host, 'premium/' if 'premium' in host else '')
84 login_page = self._download_webpage(
85 login_url, None, f'Downloading {site} login page')
87 def is_logged(webpage):
88 return any(re.search(p, webpage) for p in (
89 r'id="profileMenuDropdown
"',
90 r'class="ph
-icon
-logout
"'))
92 if is_logged(login_page):
93 self._logged_in = True
96 login_form = self._hidden_inputs(login_page)
100 'password': password,
103 response = self._download_json(
104 f'https://www.{host}/front/authenticate', None,
105 f'Logging in to {site}',
106 data=urlencode_postdata(login_form),
108 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
109 'Referer': login_url,
110 'X-Requested-With': 'XMLHttpRequest',
113 if response.get('success') == '1':
114 self._logged_in = True
117 message = response.get('message')
118 if message is not None:
119 raise ExtractorError(
120 f'Unable to login: {message}', expected=True)
122 raise ExtractorError('Unable to log in')
125 class PornHubIE(PornHubBaseIE):
126 IE_DESC = 'PornHub and Thumbzilla'
127 _VALID_URL = rf'''(?x)
131 {PornHubBaseIE._PORNHUB_HOST_RE}
132 /(?:(?:view_video\.php|video/show)\?viewkey=|embed/)|
133 (?:www\.)?thumbzilla\.com/video/
137 _EMBED_REGEX = [r'<iframe[^>]+?src=["\'](?P
<url
>(?
:https?
:)?
//(?
:www\
.)?
pornhub(?
:premium
)?\
.(?
:com|net|org
)/embed
/[\da
-z
]+)']
139 'url
': 'http
://www
.pornhub
.com
/view_video
.php?viewkey
=648719015',
140 'md5
': 'a6391306d050e4547f62b3f485dd9ba9
',
144 'title
': 'Seductive Indian beauty strips down
and fingers her pink pussy
',
146 'upload_date
': '20130628',
147 'timestamp
': 1372447216,
151 'dislike_count
': int,
152 'comment_count
': int,
160 'url
': 'http
://www
.pornhub
.com
/view_video
.php?viewkey
=1331683002',
165 'upload_date
': '20150213',
166 'timestamp
': 1423804862,
170 'dislike_count
': int,
171 'comment_count
': int,
177 'skip_download
': True,
179 'skip
': 'Video has been flagged
for verification
in accordance with our trust
and safety policy
',
182 'url
': 'https
://www
.pornhub
.com
/view_video
.php?viewkey
=ph5af5fef7c2aa7
',
184 'id': 'ph5af5fef7c2aa7
',
186 'title
': 'BFFS
- Cute Teen Girls Share Cock On the Floor
',
191 'dislike_count
': int,
192 'comment_count
': int,
203 'skip_download
': True,
205 'skip
': 'This video has been disabled
',
207 'url
': 'http
://www
.pornhub
.com
/view_video
.php?viewkey
=ph601dc30bae19a
',
209 'id': 'ph601dc30bae19a
',
210 'uploader
': 'Projekt Melody
',
211 'uploader_id
': 'projekt
-melody
',
212 'upload_date
': '20210205',
213 'title
': '"Welcome to My Pussy Mansion" - CB
Stream (02/03/21)',
214 'thumbnail
': r're
:https?
://.+',
217 'url
': 'http
://www
.pornhub
.com
/view_video
.php?viewkey
=ph557bbb6676d2d
',
218 'only_matching
': True,
220 # removed at the request of cam4.com
221 'url
': 'http
://fr
.pornhub
.com
/view_video
.php?viewkey
=ph55ca2f9760862
',
222 'only_matching
': True,
224 # removed at the request of the copyright owner
225 'url
': 'http
://www
.pornhub
.com
/view_video
.php?viewkey
=788152859',
226 'only_matching
': True,
228 # removed by uploader
229 'url
': 'http
://www
.pornhub
.com
/view_video
.php?viewkey
=ph572716d15a111
',
230 'only_matching
': True,
233 'url
': 'http
://www
.pornhub
.com
/view_video
.php?viewkey
=ph56fd731fce6b7
',
234 'only_matching
': True,
236 'url
': 'https
://www
.thumbzilla
.com
/video
/ph56c6114abd99a
/horny
-girlfriend
-sex
',
237 'only_matching
': True,
239 'url
': 'http
://www
.pornhub
.com
/video
/show?viewkey
=648719015',
240 'only_matching
': True,
242 'url
': 'https
://www
.pornhub
.net
/view_video
.php?viewkey
=203640933',
243 'only_matching
': True,
245 'url
': 'https
://www
.pornhub
.org
/view_video
.php?viewkey
=203640933',
246 'only_matching
': True,
248 'url
': 'https
://www
.pornhubpremium
.com
/view_video
.php?viewkey
=ph5e4acdae54a82
',
249 'only_matching
': True,
251 # Some videos are available with the same id on both premium
252 # and non-premium sites (e.g. this and the following test)
253 'url
': 'https
://www
.pornhub
.com
/view_video
.php?viewkey
=ph5f75b0f4b18e3
',
254 'only_matching
': True,
256 'url
': 'https
://www
.pornhubpremium
.com
/view_video
.php?viewkey
=ph5f75b0f4b18e3
',
257 'only_matching
': True,
260 'url
': 'https
://www
.pornhub
.com
/view_video
.php?viewkey
=ph5a9813bfa7156
',
261 'only_matching
': True,
263 'url
': 'http
://pornhubvybmsymdol4iibwgwtkpwmeyd6luq2gxajgjzfjvotyt5zhyd
.onion
/view_video
.php?viewkey
=ph5a9813bfa7156
',
264 'only_matching
': True,
267 def _extract_count(self, pattern, webpage, name):
268 return str_to_int(self._search_regex(pattern, webpage, f'{name} count
', default=None))
270 def _real_extract(self, url):
271 mobj = self._match_valid_url(url)
272 host = mobj.group('host
') or 'pornhub
.com
'
273 video_id = mobj.group('id')
276 self._set_age_cookies(host)
278 def dl_webpage(platform):
279 self._set_cookie(host, 'platform
', platform)
280 return self._download_webpage(
281 f'https
://www
.{host}
/view_video
.php?viewkey
={video_id}
',
282 video_id, f'Downloading {platform} webpage
')
284 webpage = dl_webpage('pc
')
286 error_msg = self._html_search_regex(
287 (r'(?s
)<div
[^
>]+class=(["\'])(?:(?!\1).)*\b(?:removed|userMessageSection)\b(?:(?!\1).)*\1[^>]*>(?P<error>.+?)</div>',
288 r'(?s)<section[^>]+class=["\']noVideo
["\'][^>]*>(?P<error>.+?)</section>'),
289 webpage, 'error message', default=None, group='error')
291 error_msg = re.sub(r'\s+', ' ', error_msg)
292 raise ExtractorError(
293 f'PornHub said: {error_msg}',
294 expected=True, video_id=video_id)
296 if any(re.search(p, webpage) for p in (
297 r'class=["\']geoBlocked
["\']',
298 r'>\s*This content is unavailable in your country')):
299 self.raise_geo_restricted()
301 # video_title from flashvars contains whitespace instead of non-ASCII (see
302 # http://www.pornhub.com/view_video.php?viewkey=1331683002), not relying
304 title = self._html_search_meta(
305 'twitter:title', webpage, default=None) or self._html_search_regex(
306 (r'(?s)<h1[^>]+class=["\']title
["\'][^>]*>(?P<title>.+?)</h1>',
307 r'<div[^>]+data-video-title=(["\'])(?P
<title
>(?
:(?
!\
1).)+)\
1',
308 r'shareTitle
["\']\s*[=:]\s*(["\'])(?P
<title
>(?
:(?
!\
1).)+)\
1'),
309 webpage, 'title
', group='title
')
312 video_urls_set = set()
315 flashvars = self._parse_json(
317 r'var\s
+flashvars_\d
+\s
*=\s
*({.+?
});', webpage, 'flashvars
', default='{}'),
320 subtitle_url = url_or_none(flashvars.get('closedCaptionsFile
'))
322 subtitles.setdefault('en
', []).append({
326 thumbnail = flashvars.get('image_url
')
327 duration = int_or_none(flashvars.get('video_duration
'))
328 media_definitions = flashvars.get('mediaDefinitions
')
329 if isinstance(media_definitions, list):
330 for definition in media_definitions:
331 if not isinstance(definition, dict):
333 video_url = definition.get('videoUrl
')
334 if not video_url or not isinstance(video_url, str):
336 if video_url in video_urls_set:
338 video_urls_set.add(video_url)
340 (video_url, int_or_none(definition.get('quality
'))))
342 thumbnail, duration = [None] * 2
344 def extract_js_vars(webpage, pattern, default=NO_DEFAULT):
345 assignments = self._search_regex(
346 pattern, webpage, 'encoded url
', default=default)
350 assignments = assignments.split(';')
354 def parse_js_value(inp):
355 inp = re.sub(r'/\
*(?
:(?
!\
*/).)*?\
*/', '', inp)
357 inps = inp.split('+')
358 return functools.reduce(
359 operator.concat, map(parse_js_value, inps))
363 return remove_quotes(inp)
365 for assn in assignments:
369 assn = re.sub(r'var\s
+', '', assn)
370 vname, value = assn.split('=', 1)
371 js_vars[vname] = parse_js_value(value)
374 def add_video_url(video_url):
375 v_url = url_or_none(video_url)
378 if v_url in video_urls_set:
380 video_urls.append((v_url, None))
381 video_urls_set.add(v_url)
383 def parse_quality_items(quality_items):
384 q_items = self._parse_json(quality_items, video_id, fatal=False)
385 if not isinstance(q_items, list):
388 if isinstance(item, dict):
389 add_video_url(item.get('url
'))
392 FORMAT_PREFIXES = ('media
', 'quality
', 'qualityItems
')
393 js_vars = extract_js_vars(
394 webpage, r'(var\s
+(?
:{})_
.+)'.format('|
'.join(FORMAT_PREFIXES)),
397 for key, format_url in js_vars.items():
398 if key.startswith(FORMAT_PREFIXES[-1]):
399 parse_quality_items(format_url)
400 elif any(key.startswith(p) for p in FORMAT_PREFIXES[:2]):
401 add_video_url(format_url)
402 if not video_urls and re.search(
403 r'<[^
>]+\bid
=["\']lockedPlayer', webpage):
404 raise ExtractorError(
405 f'Video {video_id} is locked', expected=True)
408 js_vars = extract_js_vars(
409 dl_webpage('tv'), r'(var.+?mediastring.+?)</script>')
410 add_video_url(js_vars['mediastring'])
412 for mobj in re.finditer(
413 r'<a[^>]+\bclass=["\']downloadBtn
\b[^
>]+\bhref
=(["\'])(?P<url>(?:(?!\1).)+)\1',
415 video_url = mobj.group('url')
416 if video_url not in video_urls_set:
417 video_urls.append((video_url, None))
418 video_urls_set.add(video_url)
423 def add_format(format_url, height=None):
424 ext = determine_ext(format_url)
426 formats.extend(self._extract_mpd_formats(
427 format_url, video_id, mpd_id='dash', fatal=False))
430 formats.extend(self._extract_m3u8_formats(
431 format_url, video_id, 'mp4', entry_protocol='m3u8_native',
432 m3u8_id='hls', fatal=False))
435 height = int_or_none(self._search_regex(
436 r'(?P<height>\d+)[pP]?_\d+[kK]', format_url, 'height',
440 'format_id': format_field(height, None, '%dp'),
444 for video_url, height in video_urls:
446 upload_date = self._search_regex(
447 r'/(\d{6}/\d{2})/', video_url, 'upload data', default=None)
449 upload_date = upload_date.replace('/', '')
450 if '/video/get_media' in video_url:
451 medias = self._download_json(video_url, video_id, fatal=False)
452 if isinstance(medias, list):
454 if not isinstance(media, dict):
456 video_url = url_or_none(media.get('videoUrl'))
459 height = int_or_none(media.get('quality'))
460 add_format(video_url, height)
462 add_format(video_url)
464 model_profile = self._search_json(
465 r'var\s+MODEL_PROFILE\s*=', webpage, 'model profile', video_id, fatal=False)
466 video_uploader = self._html_search_regex(
467 r'(?s)From: .+?<(?:a\b[^>]+\bhref=["\']/(?
:(?
:user|channel
)s|model|pornstar
)/|span
\b[^
>]+\bclass
=["\']username)[^>]+>(.+?)<',
468 webpage, 'uploader', default=None) or model_profile.get('username')
470 def extract_vote_count(kind, name):
471 return self._extract_count(
472 (rf'<span[^>]+\bclass="votes{kind}
"[^>]*>([\d,\.]+)</span>',
473 rf'<span[^>]+\bclass=["\']votes{kind}
["\'][^>]*\bdata-rating=["\'](\d
+)'),
476 view_count = self._extract_count(
477 r'<span
class="count">([\d
,\
.]+)</span
> [Vv
]iews
', webpage, 'view
')
478 like_count = extract_vote_count('Up
', 'like
')
479 dislike_count = extract_vote_count('Down
', 'dislike
')
480 comment_count = self._extract_count(
481 r'All Comments\s
*<span
>\
(([\d
,.]+)\
)', webpage, 'comment
')
483 def extract_list(meta_key):
484 div = self._search_regex(
485 rf'(?s
)<div
[^
>]+\bclass
=["\'].*?\b{meta_key}Wrapper[^>]*>(.+?)</div>',
486 webpage, meta_key, default=None)
488 return [clean_html(x).strip() for x in re.findall(r'(?s)<a[^>]+\bhref=[^>]+>.+?</a>', div)]
490 info = self._search_json_ld(webpage, video_id, default={})
491 # description provided in JSON-LD is irrelevant
492 info['description'] = None
496 'uploader': video_uploader,
497 'uploader_id': remove_start(model_profile.get('modelProfileLink'), '/model/'),
498 'upload_date': upload_date,
500 'thumbnail': thumbnail,
501 'duration': duration,
502 'view_count': view_count,
503 'like_count': like_count,
504 'dislike_count': dislike_count,
505 'comment_count': comment_count,
508 'tags': extract_list('tags'),
509 'categories': extract_list('categories'),
510 'cast': extract_list('pornstars'),
511 'subtitles': subtitles,
515 class PornHubPlaylistBaseIE(PornHubBaseIE):
516 def _extract_page(self, url):
517 return int_or_none(self._search_regex(
518 r'\bpage=(\d+)', url, 'page', default=None))
520 def _extract_entries(self, webpage, host):
521 # Only process container div with main playlist content skipping
522 # drop-down menu that uses similar pattern for videos (see
523 # https://github.com/ytdl-org/youtube-dl/issues/11594).
524 container = self._search_regex(
525 r'(?s)(<div[^>]+class=["\']container
.+)', webpage,
526 'container
', default=webpage)
530 f'http
://www
.{host}
/{video_url}
',
531 PornHubIE.ie_key(), video_title=title)
532 for video_url, title in orderedSet(re.findall(
533 r'href
="/?(view_video\.php\?.*\bviewkey=[\da-z]+[^"]*)"[^>]*\s+title="([^
"]+)"',
538 class PornHubUserIE(PornHubPlaylistBaseIE):
539 _VALID_URL = rf'(?P
<url
>https?
://(?
:[^
/]+\
.)?
{PornHubBaseIE
._PORNHUB
_HOST
_RE
}/(?
:(?
:user|channel
)s|model|pornstar
)/(?P
<id>[^
/?
#&]+))(?:[?#&]|/(?!videos)|$)'
541 'url': 'https://www.pornhub.com/model/zoe_ph',
542 'playlist_mincount': 118,
544 'url': 'https://www.pornhub.com/pornstar/liz-vicious',
548 'playlist_mincount': 118,
550 'url': 'https://www.pornhub.com/users/russianveet69',
551 'only_matching': True,
553 'url': 'https://www.pornhub.com/channels/povd',
554 'only_matching': True,
556 'url': 'https://www.pornhub.com/model/zoe_ph?abc=1',
557 'only_matching': True,
559 # Unavailable via /videos page, but available with direct pagination
560 # on pornstar page (see [1]), requires premium
561 # 1. https://github.com/ytdl-org/youtube-dl/issues/27853
562 'url': 'https://www.pornhubpremium.com/pornstar/sienna-west',
563 'only_matching': True,
565 # Same as before, multi page
566 'url': 'https://www.pornhubpremium.com/pornstar/lily-labeau',
567 'only_matching': True,
569 'url': 'https://pornhubvybmsymdol4iibwgwtkpwmeyd6luq2gxajgjzfjvotyt5zhyd.onion/model/zoe_ph',
570 'only_matching': True,
573 def _real_extract(self
, url
):
574 mobj
= self
._match
_valid
_url
(url
)
575 user_id
= mobj
.group('id')
576 videos_url
= '{}/videos'.format(mobj
.group('url'))
577 self
._set
_age
_cookies
(mobj
.group('host'))
578 page
= self
._extract
_page
(url
)
580 videos_url
= update_url_query(videos_url
, {'page': page
})
581 return self
.url_result(
582 videos_url
, ie
=PornHubPagedVideoListIE
.ie_key(), video_id
=user_id
)
585 class PornHubPagedPlaylistBaseIE(PornHubPlaylistBaseIE
):
587 def _has_more(webpage
):
590 <li[^>]+\bclass=["\']page_next|
591 <link
[^
>]+\brel
=["\']next|
592 <button[^>]+\bid=["\']moreDataBtn
593 ''', webpage) is not None
595 def _entries(self, url, host, item_id):
596 page = self._extract_page(url)
600 def download_page(base_url, num, fallback=False):
601 note = 'Downloading page {}{}'.format(num, ' (switch to fallback)' if fallback else '')
602 return self._download_webpage(
603 base_url, item_id, note, query={'page': num})
606 return isinstance(e.cause, HTTPError) and e.cause.status == 404
609 has_page = page is not None
610 first_page = page if has_page else 1
611 for page_num in (first_page, ) if has_page else itertools.count(first_page):
614 webpage = download_page(base_url, page_num)
615 except ExtractorError as e:
616 # Some sources may not be available via /videos page,
617 # trying to fallback to main page pagination (see [1])
618 # 1. https://github.com/ytdl-org/youtube-dl/issues/27853
619 if is_404(e) and page_num == first_page and VIDEOS in base_url:
620 base_url = base_url.replace(VIDEOS, '')
621 webpage = download_page(base_url, page_num, fallback=True)
624 except ExtractorError as e:
625 if is_404(e) and page_num != first_page:
628 page_entries = self._extract_entries(webpage, host)
631 for e in page_entries:
633 if not self._has_more(webpage):
636 def _real_extract(self, url):
637 mobj = self._match_valid_url(url)
638 host = mobj.group('host')
639 item_id = mobj.group('id')
642 self._set_age_cookies(host)
644 return self.playlist_result(self._entries(url, host, item_id), item_id)
647 class PornHubPagedVideoListIE(PornHubPagedPlaylistBaseIE):
648 _VALID_URL = rf'https?://(?:[^/]+\.)?{PornHubBaseIE._PORNHUB_HOST_RE}/(?!playlist/)(?P<id>(?:[^/]+/)*[^/?#&]+)'
650 'url': 'https://www.pornhub.com/model/zoe_ph/videos',
651 'only_matching': True,
653 'url': 'http://www.pornhub.com/users/rushandlia/videos',
654 'only_matching': True,
656 'url': 'https://www.pornhub.com/pornstar/jenny-blighe/videos',
658 'id': 'pornstar/jenny-blighe/videos',
660 'playlist_mincount': 149,
662 'url': 'https://www.pornhub.com/pornstar/jenny-blighe/videos?page=3',
664 'id': 'pornstar/jenny-blighe/videos',
666 'playlist_mincount': 40,
668 # default sorting as Top Rated Videos
669 'url': 'https://www.pornhub.com/channels/povd/videos',
671 'id': 'channels/povd/videos',
673 'playlist_mincount': 293,
676 'url': 'https://www.pornhub.com/channels/povd/videos?o=ra',
677 'only_matching': True,
680 'url': 'https://www.pornhub.com/channels/povd/videos?o=da',
681 'only_matching': True,
684 'url': 'https://www.pornhub.com/channels/povd/videos?o=vi',
685 'only_matching': True,
687 'url': 'http://www.pornhub.com/users/zoe_ph/videos/public',
688 'only_matching': True,
691 'url': 'https://www.pornhub.com/pornstar/liz-vicious/videos?o=mv',
692 'only_matching': True,
695 'url': 'https://www.pornhub.com/pornstar/liz-vicious/videos?o=tr',
696 'only_matching': True,
699 'url': 'https://www.pornhub.com/pornstar/liz-vicious/videos?o=lg',
700 'only_matching': True,
703 'url': 'https://www.pornhub.com/pornstar/liz-vicious/videos?o=cm',
704 'only_matching': True,
706 'url': 'https://www.pornhub.com/pornstar/liz-vicious/videos/paid',
707 'only_matching': True,
709 'url': 'https://www.pornhub.com/pornstar/liz-vicious/videos/fanonly',
710 'only_matching': True,
712 'url': 'https://www.pornhub.com/video',
713 'only_matching': True,
715 'url': 'https://www.pornhub.com/video?page=3',
716 'only_matching': True,
718 'url': 'https://www.pornhub.com/video/search?search=123',
719 'only_matching': True,
721 'url': 'https://www.pornhub.com/categories/teen',
722 'only_matching': True,
724 'url': 'https://www.pornhub.com/categories/teen?page=3',
725 'only_matching': True,
727 'url': 'https://www.pornhub.com/hd',
728 'only_matching': True,
730 'url': 'https://www.pornhub.com/hd?page=3',
731 'only_matching': True,
733 'url': 'https://www.pornhub.com/described-video',
734 'only_matching': True,
736 'url': 'https://www.pornhub.com/described-video?page=2',
737 'only_matching': True,
739 'url': 'https://www.pornhub.com/video/incategories/60fps-1/hd-porn',
740 'only_matching': True,
742 'url': 'https://pornhubvybmsymdol4iibwgwtkpwmeyd6luq2gxajgjzfjvotyt5zhyd.onion/model/zoe_ph/videos',
743 'only_matching': True,
747 def suitable(cls, url):
749 if PornHubIE.suitable(url) or PornHubUserIE.suitable(url) or PornHubUserVideosUploadIE.suitable(url)
750 else super().suitable(url))
753 class PornHubUserVideosUploadIE(PornHubPagedPlaylistBaseIE):
754 _VALID_URL = rf'(?P<url>https?://(?:[^/]+\.)?{PornHubBaseIE._PORNHUB_HOST_RE}/(?:(?:user|channel)s|model|pornstar)/(?P<id>[^/]+)/videos/upload)'
756 'url': 'https://www.pornhub.com/pornstar/jenny-blighe/videos/upload',
758 'id': 'jenny-blighe',
760 'playlist_mincount': 129,
762 'url': 'https://www.pornhub.com/model/zoe_ph/videos/upload',
763 'only_matching': True,
765 'url': 'http://pornhubvybmsymdol4iibwgwtkpwmeyd6luq2gxajgjzfjvotyt5zhyd.onion/pornstar/jenny-blighe/videos/upload',
766 'only_matching': True,
770 class PornHubPlaylistIE(PornHubPlaylistBaseIE):
771 _VALID_URL = rf'(?P<url>https?://(?:[^/]+\.)?{PornHubBaseIE._PORNHUB_HOST_RE}/playlist/(?P<id>[^/?#&]+))'
773 'url': 'https://www.pornhub.com/playlist/44121572',
777 'playlist_count': 77,
779 'url': 'https://www.pornhub.com/playlist/4667351',
780 'only_matching': True,
782 'url': 'https://de.pornhub.com/playlist/4667351',
783 'only_matching': True,
785 'url': 'https://de.pornhub.com/playlist/4667351?page=2',
786 'only_matching': True,
789 def _entries(self, url, host, item_id):
790 webpage = self._download_webpage(url, item_id, 'Downloading page 1')
791 playlist_id = self._search_regex(r'var\s+playlistId\s*=\s*"([^"]+)"', webpage, 'playlist_id')
792 video_count = int_or_none(
793 self._search_regex(r'var\s+itemsCount\s*=\s*([0-9]+)\s*\|\|', webpage, 'video_count'))
794 token = self._search_regex(r'var\s+token\s*=\s*"([^"]+)"', webpage, 'token')
795 page_count = math.ceil((video_count - 36) / 40.) + 1
796 page_entries = self._extract_entries(webpage, host)
798 def download_page(page_num):
799 note = f'Downloading page {page_num}'
800 page_url = f'https://www.{host}/playlist/viewChunked'
801 return self._download_webpage(page_url, item_id, note, query={
807 for page_num in range(1, page_count + 1):
809 webpage = download_page(page_num)
810 page_entries = self._extract_entries(webpage, host)
813 yield from page_entries
815 def _real_extract(self, url):
816 mobj = self._match_valid_url(url)
817 host = mobj.group('host')
818 item_id = mobj.group('id')
821 self._set_age_cookies(host)
823 return self.playlist_result(self._entries(mobj.group('url'), host, item_id), item_id)