]> jfr.im git - yt-dlp.git/blob - youtube_dl/extractor/pornhub.py
Start moving to ytdl-org
[yt-dlp.git] / youtube_dl / extractor / pornhub.py
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 ExtractorError,
18 int_or_none,
19 orderedSet,
20 remove_quotes,
21 str_to_int,
22 url_or_none,
23 )
24
25
26 class PornHubBaseIE(InfoExtractor):
27 def _download_webpage_handle(self, *args, **kwargs):
28 def dl(*args, **kwargs):
29 return super(PornHubBaseIE, self)._download_webpage_handle(*args, **kwargs)
30
31 webpage, urlh = dl(*args, **kwargs)
32
33 if any(re.search(p, webpage) for p in (
34 r'<body\b[^>]+\bonload=["\']go\(\)',
35 r'document\.cookie\s*=\s*["\']RNKEY=',
36 r'document\.location\.reload\(true\)')):
37 url_or_request = args[0]
38 url = (url_or_request.get_full_url()
39 if isinstance(url_or_request, compat_urllib_request.Request)
40 else url_or_request)
41 phantom = PhantomJSwrapper(self, required_version='2.0')
42 phantom.get(url, html=webpage)
43 webpage, urlh = dl(*args, **kwargs)
44
45 return webpage, urlh
46
47
48 class PornHubIE(PornHubBaseIE):
49 IE_DESC = 'PornHub and Thumbzilla'
50 _VALID_URL = r'''(?x)
51 https?://
52 (?:
53 (?:[^/]+\.)?(?P<host>pornhub\.(?:com|net))/(?:(?:view_video\.php|video/show)\?viewkey=|embed/)|
54 (?:www\.)?thumbzilla\.com/video/
55 )
56 (?P<id>[\da-z]+)
57 '''
58 _TESTS = [{
59 'url': 'http://www.pornhub.com/view_video.php?viewkey=648719015',
60 'md5': '1e19b41231a02eba417839222ac9d58e',
61 'info_dict': {
62 'id': '648719015',
63 'ext': 'mp4',
64 'title': 'Seductive Indian beauty strips down and fingers her pink pussy',
65 'uploader': 'Babes',
66 'upload_date': '20130628',
67 'duration': 361,
68 'view_count': int,
69 'like_count': int,
70 'dislike_count': int,
71 'comment_count': int,
72 'age_limit': 18,
73 'tags': list,
74 'categories': list,
75 },
76 }, {
77 # non-ASCII title
78 'url': 'http://www.pornhub.com/view_video.php?viewkey=1331683002',
79 'info_dict': {
80 'id': '1331683002',
81 'ext': 'mp4',
82 'title': '重庆婷婷女王足交',
83 'uploader': 'Unknown',
84 'upload_date': '20150213',
85 'duration': 1753,
86 'view_count': int,
87 'like_count': int,
88 'dislike_count': int,
89 'comment_count': int,
90 'age_limit': 18,
91 'tags': list,
92 'categories': list,
93 },
94 'params': {
95 'skip_download': True,
96 },
97 }, {
98 # subtitles
99 'url': 'https://www.pornhub.com/view_video.php?viewkey=ph5af5fef7c2aa7',
100 'info_dict': {
101 'id': 'ph5af5fef7c2aa7',
102 'ext': 'mp4',
103 'title': 'BFFS - Cute Teen Girls Share Cock On the Floor',
104 'uploader': 'BFFs',
105 'duration': 622,
106 'view_count': int,
107 'like_count': int,
108 'dislike_count': int,
109 'comment_count': int,
110 'age_limit': 18,
111 'tags': list,
112 'categories': list,
113 'subtitles': {
114 'en': [{
115 "ext": 'srt'
116 }]
117 },
118 },
119 'params': {
120 'skip_download': True,
121 },
122 }, {
123 'url': 'http://www.pornhub.com/view_video.php?viewkey=ph557bbb6676d2d',
124 'only_matching': True,
125 }, {
126 # removed at the request of cam4.com
127 'url': 'http://fr.pornhub.com/view_video.php?viewkey=ph55ca2f9760862',
128 'only_matching': True,
129 }, {
130 # removed at the request of the copyright owner
131 'url': 'http://www.pornhub.com/view_video.php?viewkey=788152859',
132 'only_matching': True,
133 }, {
134 # removed by uploader
135 'url': 'http://www.pornhub.com/view_video.php?viewkey=ph572716d15a111',
136 'only_matching': True,
137 }, {
138 # private video
139 'url': 'http://www.pornhub.com/view_video.php?viewkey=ph56fd731fce6b7',
140 'only_matching': True,
141 }, {
142 'url': 'https://www.thumbzilla.com/video/ph56c6114abd99a/horny-girlfriend-sex',
143 'only_matching': True,
144 }, {
145 'url': 'http://www.pornhub.com/video/show?viewkey=648719015',
146 'only_matching': True,
147 }, {
148 'url': 'https://www.pornhub.net/view_video.php?viewkey=203640933',
149 'only_matching': True,
150 }]
151
152 @staticmethod
153 def _extract_urls(webpage):
154 return re.findall(
155 r'<iframe[^>]+?src=["\'](?P<url>(?:https?:)?//(?:www\.)?pornhub\.(?:com|net)/embed/[\da-z]+)',
156 webpage)
157
158 def _extract_count(self, pattern, webpage, name):
159 return str_to_int(self._search_regex(
160 pattern, webpage, '%s count' % name, fatal=False))
161
162 def _real_extract(self, url):
163 mobj = re.match(self._VALID_URL, url)
164 host = mobj.group('host') or 'pornhub.com'
165 video_id = mobj.group('id')
166
167 self._set_cookie(host, 'age_verified', '1')
168
169 def dl_webpage(platform):
170 self._set_cookie(host, 'platform', platform)
171 return self._download_webpage(
172 'http://www.%s/view_video.php?viewkey=%s' % (host, video_id),
173 video_id, 'Downloading %s webpage' % platform)
174
175 webpage = dl_webpage('pc')
176
177 error_msg = self._html_search_regex(
178 r'(?s)<div[^>]+class=(["\'])(?:(?!\1).)*\b(?:removed|userMessageSection)\b(?:(?!\1).)*\1[^>]*>(?P<error>.+?)</div>',
179 webpage, 'error message', default=None, group='error')
180 if error_msg:
181 error_msg = re.sub(r'\s+', ' ', error_msg)
182 raise ExtractorError(
183 'PornHub said: %s' % error_msg,
184 expected=True, video_id=video_id)
185
186 # video_title from flashvars contains whitespace instead of non-ASCII (see
187 # http://www.pornhub.com/view_video.php?viewkey=1331683002), not relying
188 # on that anymore.
189 title = self._html_search_meta(
190 'twitter:title', webpage, default=None) or self._search_regex(
191 (r'<h1[^>]+class=["\']title["\'][^>]*>(?P<title>[^<]+)',
192 r'<div[^>]+data-video-title=(["\'])(?P<title>.+?)\1',
193 r'shareTitle\s*=\s*(["\'])(?P<title>.+?)\1'),
194 webpage, 'title', group='title')
195
196 video_urls = []
197 video_urls_set = set()
198 subtitles = {}
199
200 flashvars = self._parse_json(
201 self._search_regex(
202 r'var\s+flashvars_\d+\s*=\s*({.+?});', webpage, 'flashvars', default='{}'),
203 video_id)
204 if flashvars:
205 subtitle_url = url_or_none(flashvars.get('closedCaptionsFile'))
206 if subtitle_url:
207 subtitles.setdefault('en', []).append({
208 'url': subtitle_url,
209 'ext': 'srt',
210 })
211 thumbnail = flashvars.get('image_url')
212 duration = int_or_none(flashvars.get('video_duration'))
213 media_definitions = flashvars.get('mediaDefinitions')
214 if isinstance(media_definitions, list):
215 for definition in media_definitions:
216 if not isinstance(definition, dict):
217 continue
218 video_url = definition.get('videoUrl')
219 if not video_url or not isinstance(video_url, compat_str):
220 continue
221 if video_url in video_urls_set:
222 continue
223 video_urls_set.add(video_url)
224 video_urls.append(
225 (video_url, int_or_none(definition.get('quality'))))
226 else:
227 thumbnail, duration = [None] * 2
228
229 if not video_urls:
230 tv_webpage = dl_webpage('tv')
231
232 assignments = self._search_regex(
233 r'(var.+?mediastring.+?)</script>', tv_webpage,
234 'encoded url').split(';')
235
236 js_vars = {}
237
238 def parse_js_value(inp):
239 inp = re.sub(r'/\*(?:(?!\*/).)*?\*/', '', inp)
240 if '+' in inp:
241 inps = inp.split('+')
242 return functools.reduce(
243 operator.concat, map(parse_js_value, inps))
244 inp = inp.strip()
245 if inp in js_vars:
246 return js_vars[inp]
247 return remove_quotes(inp)
248
249 for assn in assignments:
250 assn = assn.strip()
251 if not assn:
252 continue
253 assn = re.sub(r'var\s+', '', assn)
254 vname, value = assn.split('=', 1)
255 js_vars[vname] = parse_js_value(value)
256
257 video_url = js_vars['mediastring']
258 if video_url not in video_urls_set:
259 video_urls.append((video_url, None))
260 video_urls_set.add(video_url)
261
262 for mobj in re.finditer(
263 r'<a[^>]+\bclass=["\']downloadBtn\b[^>]+\bhref=(["\'])(?P<url>(?:(?!\1).)+)\1',
264 webpage):
265 video_url = mobj.group('url')
266 if video_url not in video_urls_set:
267 video_urls.append((video_url, None))
268 video_urls_set.add(video_url)
269
270 upload_date = None
271 formats = []
272 for video_url, height in video_urls:
273 if not upload_date:
274 upload_date = self._search_regex(
275 r'/(\d{6}/\d{2})/', video_url, 'upload data', default=None)
276 if upload_date:
277 upload_date = upload_date.replace('/', '')
278 tbr = None
279 mobj = re.search(r'(?P<height>\d+)[pP]?_(?P<tbr>\d+)[kK]', video_url)
280 if mobj:
281 if not height:
282 height = int(mobj.group('height'))
283 tbr = int(mobj.group('tbr'))
284 formats.append({
285 'url': video_url,
286 'format_id': '%dp' % height if height else None,
287 'height': height,
288 'tbr': tbr,
289 })
290 self._sort_formats(formats)
291
292 video_uploader = self._html_search_regex(
293 r'(?s)From:&nbsp;.+?<(?:a\b[^>]+\bhref=["\']/(?:(?:user|channel)s|model|pornstar)/|span\b[^>]+\bclass=["\']username)[^>]+>(.+?)<',
294 webpage, 'uploader', fatal=False)
295
296 view_count = self._extract_count(
297 r'<span class="count">([\d,\.]+)</span> views', webpage, 'view')
298 like_count = self._extract_count(
299 r'<span class="votesUp">([\d,\.]+)</span>', webpage, 'like')
300 dislike_count = self._extract_count(
301 r'<span class="votesDown">([\d,\.]+)</span>', webpage, 'dislike')
302 comment_count = self._extract_count(
303 r'All Comments\s*<span>\(([\d,.]+)\)', webpage, 'comment')
304
305 def extract_list(meta_key):
306 div = self._search_regex(
307 r'(?s)<div[^>]+\bclass=["\'].*?\b%sWrapper[^>]*>(.+?)</div>'
308 % meta_key, webpage, meta_key, default=None)
309 if div:
310 return re.findall(r'<a[^>]+\bhref=[^>]+>([^<]+)', div)
311
312 return {
313 'id': video_id,
314 'uploader': video_uploader,
315 'upload_date': upload_date,
316 'title': title,
317 'thumbnail': thumbnail,
318 'duration': duration,
319 'view_count': view_count,
320 'like_count': like_count,
321 'dislike_count': dislike_count,
322 'comment_count': comment_count,
323 'formats': formats,
324 'age_limit': 18,
325 'tags': extract_list('tags'),
326 'categories': extract_list('categories'),
327 'subtitles': subtitles,
328 }
329
330
331 class PornHubPlaylistBaseIE(PornHubBaseIE):
332 def _extract_entries(self, webpage, host):
333 # Only process container div with main playlist content skipping
334 # drop-down menu that uses similar pattern for videos (see
335 # https://github.com/ytdl-org/youtube-dl/issues/11594).
336 container = self._search_regex(
337 r'(?s)(<div[^>]+class=["\']container.+)', webpage,
338 'container', default=webpage)
339
340 return [
341 self.url_result(
342 'http://www.%s/%s' % (host, video_url),
343 PornHubIE.ie_key(), video_title=title)
344 for video_url, title in orderedSet(re.findall(
345 r'href="/?(view_video\.php\?.*\bviewkey=[\da-z]+[^"]*)"[^>]*\s+title="([^"]+)"',
346 container))
347 ]
348
349 def _real_extract(self, url):
350 mobj = re.match(self._VALID_URL, url)
351 host = mobj.group('host')
352 playlist_id = mobj.group('id')
353
354 webpage = self._download_webpage(url, playlist_id)
355
356 entries = self._extract_entries(webpage, host)
357
358 playlist = self._parse_json(
359 self._search_regex(
360 r'(?:playlistObject|PLAYLIST_VIEW)\s*=\s*({.+?});', webpage,
361 'playlist', default='{}'),
362 playlist_id, fatal=False)
363 title = playlist.get('title') or self._search_regex(
364 r'>Videos\s+in\s+(.+?)\s+[Pp]laylist<', webpage, 'title', fatal=False)
365
366 return self.playlist_result(
367 entries, playlist_id, title, playlist.get('description'))
368
369
370 class PornHubPlaylistIE(PornHubPlaylistBaseIE):
371 _VALID_URL = r'https?://(?:[^/]+\.)?(?P<host>pornhub\.(?:com|net))/playlist/(?P<id>\d+)'
372 _TESTS = [{
373 'url': 'http://www.pornhub.com/playlist/4667351',
374 'info_dict': {
375 'id': '4667351',
376 'title': 'Nataly Hot',
377 },
378 'playlist_mincount': 2,
379 }, {
380 'url': 'https://de.pornhub.com/playlist/4667351',
381 'only_matching': True,
382 }]
383
384
385 class PornHubUserVideosIE(PornHubPlaylistBaseIE):
386 _VALID_URL = r'https?://(?:[^/]+\.)?(?P<host>pornhub\.(?:com|net))/(?:(?:user|channel)s|model|pornstar)/(?P<id>[^/]+)/videos'
387 _TESTS = [{
388 'url': 'http://www.pornhub.com/users/zoe_ph/videos/public',
389 'info_dict': {
390 'id': 'zoe_ph',
391 },
392 'playlist_mincount': 171,
393 }, {
394 'url': 'http://www.pornhub.com/users/rushandlia/videos',
395 'only_matching': True,
396 }, {
397 # default sorting as Top Rated Videos
398 'url': 'https://www.pornhub.com/channels/povd/videos',
399 'info_dict': {
400 'id': 'povd',
401 },
402 'playlist_mincount': 293,
403 }, {
404 # Top Rated Videos
405 'url': 'https://www.pornhub.com/channels/povd/videos?o=ra',
406 'only_matching': True,
407 }, {
408 # Most Recent Videos
409 'url': 'https://www.pornhub.com/channels/povd/videos?o=da',
410 'only_matching': True,
411 }, {
412 # Most Viewed Videos
413 'url': 'https://www.pornhub.com/channels/povd/videos?o=vi',
414 'only_matching': True,
415 }, {
416 'url': 'http://www.pornhub.com/users/zoe_ph/videos/public',
417 'only_matching': True,
418 }, {
419 'url': 'https://www.pornhub.com/model/jayndrea/videos/upload',
420 'only_matching': True,
421 }, {
422 'url': 'https://www.pornhub.com/pornstar/jenny-blighe/videos/upload',
423 'only_matching': True,
424 }]
425
426 def _real_extract(self, url):
427 mobj = re.match(self._VALID_URL, url)
428 host = mobj.group('host')
429 user_id = mobj.group('id')
430
431 entries = []
432 for page_num in itertools.count(1):
433 try:
434 webpage = self._download_webpage(
435 url, user_id, 'Downloading page %d' % page_num,
436 query={'page': page_num})
437 except ExtractorError as e:
438 if isinstance(e.cause, compat_HTTPError) and e.cause.code == 404:
439 break
440 raise
441 page_entries = self._extract_entries(webpage, host)
442 if not page_entries:
443 break
444 entries.extend(page_entries)
445
446 return self.playlist_result(entries, user_id)