]>
Commit | Line | Data |
---|---|---|
1 | import itertools | |
2 | import re | |
3 | ||
4 | from .common import InfoExtractor | |
5 | from ..utils import ( | |
6 | ExtractorError, | |
7 | clean_html, | |
8 | extract_attributes, | |
9 | get_element_by_class, | |
10 | get_element_by_id, | |
11 | get_elements_html_by_class, | |
12 | int_or_none, | |
13 | merge_dicts, | |
14 | parse_count, | |
15 | parse_qs, | |
16 | traverse_obj, | |
17 | unified_strdate, | |
18 | url_or_none, | |
19 | urljoin, | |
20 | ) | |
21 | ||
22 | ||
23 | class YouPornIE(InfoExtractor): | |
24 | _VALID_URL = r'https?://(?:www\.)?youporn\.com/(?:watch|embed)/(?P<id>\d+)(?:/(?P<display_id>[^/?#&]+))?/?(?:[#?]|$)' | |
25 | _EMBED_REGEX = [r'<iframe[^>]+\bsrc=["\'](?P<url>(?:https?:)?//(?:www\.)?youporn\.com/embed/\d+)'] | |
26 | _TESTS = [{ | |
27 | 'url': 'http://www.youporn.com/watch/505835/sex-ed-is-it-safe-to-masturbate-daily/', | |
28 | 'md5': '3744d24c50438cf5b6f6d59feb5055c2', | |
29 | 'info_dict': { | |
30 | 'id': '505835', | |
31 | 'display_id': 'sex-ed-is-it-safe-to-masturbate-daily', | |
32 | 'ext': 'mp4', | |
33 | 'title': 'Sex Ed: Is It Safe To Masturbate Daily?', | |
34 | 'description': 'Love & Sex Answers: http://bit.ly/DanAndJenn -- Is It Unhealthy To Masturbate Daily?', | |
35 | 'thumbnail': r're:^https?://.*\.jpg$', | |
36 | 'duration': 210, | |
37 | 'uploader': 'Ask Dan And Jennifer', | |
38 | 'upload_date': '20101217', | |
39 | 'average_rating': int, | |
40 | 'view_count': int, | |
41 | 'categories': list, | |
42 | 'tags': list, | |
43 | 'age_limit': 18, | |
44 | }, | |
45 | 'skip': 'This video has been deactivated', | |
46 | }, { | |
47 | # Unknown uploader | |
48 | 'url': 'http://www.youporn.com/watch/561726/big-tits-awesome-brunette-on-amazing-webcam-show/?from=related3&al=2&from_id=561726&pos=4', | |
49 | 'info_dict': { | |
50 | 'id': '561726', | |
51 | 'display_id': 'big-tits-awesome-brunette-on-amazing-webcam-show', | |
52 | 'ext': 'mp4', | |
53 | 'title': 'Big Tits Awesome Brunette On amazing webcam show', | |
54 | 'description': 'http://sweetlivegirls.com Big Tits Awesome Brunette On amazing webcam show.mp4', | |
55 | 'thumbnail': r're:^https?://.*\.jpg$', | |
56 | 'uploader': 'Unknown', | |
57 | 'upload_date': '20110418', | |
58 | 'average_rating': int, | |
59 | 'view_count': int, | |
60 | 'categories': list, | |
61 | 'tags': list, | |
62 | 'age_limit': 18, | |
63 | }, | |
64 | 'params': { | |
65 | 'skip_download': True, | |
66 | }, | |
67 | 'skip': '404', | |
68 | }, { | |
69 | 'url': 'https://www.youporn.com/embed/505835/sex-ed-is-it-safe-to-masturbate-daily/', | |
70 | 'only_matching': True, | |
71 | }, { | |
72 | 'url': 'http://www.youporn.com/watch/505835', | |
73 | 'only_matching': True, | |
74 | }, { | |
75 | 'url': 'https://www.youporn.com/watch/13922959/femdom-principal/', | |
76 | 'only_matching': True, | |
77 | }, { | |
78 | 'url': 'https://www.youporn.com/watch/16290308/tinderspecial-trailer1/', | |
79 | 'info_dict': { | |
80 | 'id': '16290308', | |
81 | 'age_limit': 18, | |
82 | 'categories': [], | |
83 | 'display_id': 'tinderspecial-trailer1', | |
84 | 'duration': 298.0, | |
85 | 'ext': 'mp4', | |
86 | 'upload_date': '20201123', | |
87 | 'uploader': 'Ersties', | |
88 | 'tags': [], | |
89 | 'thumbnail': r're:https://.+\.jpg', | |
90 | 'timestamp': 1606147564, | |
91 | 'title': 'Tinder In Real Life', | |
92 | 'view_count': int, | |
93 | }, | |
94 | }] | |
95 | ||
96 | def _real_extract(self, url): | |
97 | video_id, display_id = self._match_valid_url(url).group('id', 'display_id') | |
98 | self._set_cookie('.youporn.com', 'age_verified', '1') | |
99 | webpage = self._download_webpage(f'https://www.youporn.com/watch/{video_id}', video_id) | |
100 | ||
101 | watchable = self._search_regex( | |
102 | r'''(<div\s[^>]*\bid\s*=\s*('|")?watch-container(?(2)\2|(?!-)\b)[^>]*>)''', | |
103 | webpage, 'watchability', default=None) | |
104 | if not watchable: | |
105 | msg = re.split(r'\s{2}', clean_html(get_element_by_id('mainContent', webpage)) or '')[0] | |
106 | raise ExtractorError( | |
107 | f'{self.IE_NAME} says: {msg}' if msg else 'Video unavailable', expected=True) | |
108 | ||
109 | player_vars = self._search_json(r'\bplayervars\s*:', webpage, 'player vars', video_id) | |
110 | definitions = player_vars['mediaDefinitions'] | |
111 | ||
112 | def get_format_data(data, stream_type): | |
113 | info_url = traverse_obj(data, (lambda _, v: v['format'] == stream_type, 'videoUrl', {url_or_none}, any)) | |
114 | if not info_url: | |
115 | return [] | |
116 | return traverse_obj( | |
117 | self._download_json(info_url, video_id, f'Downloading {stream_type} info JSON', fatal=False), | |
118 | lambda _, v: v['format'] == stream_type and url_or_none(v['videoUrl'])) | |
119 | ||
120 | formats = [] | |
121 | # Try to extract only the actual master m3u8 first, avoiding the duplicate single resolution "master" m3u8s | |
122 | for hls_url in traverse_obj(get_format_data(definitions, 'hls'), ( | |
123 | lambda _, v: not isinstance(v['defaultQuality'], bool), 'videoUrl'), (..., 'videoUrl')): | |
124 | formats.extend(self._extract_m3u8_formats(hls_url, video_id, 'mp4', fatal=False, m3u8_id='hls')) | |
125 | ||
126 | for definition in get_format_data(definitions, 'mp4'): | |
127 | f = traverse_obj(definition, { | |
128 | 'url': 'videoUrl', | |
129 | 'filesize': ('videoSize', {int_or_none}), | |
130 | }) | |
131 | height = int_or_none(definition.get('quality')) | |
132 | # Video URL's path looks like this: | |
133 | # /201012/17/505835/720p_1500k_505835/YouPorn%20-%20Sex%20Ed%20Is%20It%20Safe%20To%20Masturbate%20Daily.mp4 | |
134 | # /201012/17/505835/vl_240p_240k_505835/YouPorn%20-%20Sex%20Ed%20Is%20It%20Safe%20To%20Masturbate%20Daily.mp4 | |
135 | # /videos/201703/11/109285532/1080P_4000K_109285532.mp4 | |
136 | # We will benefit from it by extracting some metadata | |
137 | mobj = re.search(r'(?P<height>\d{3,4})[pP]_(?P<bitrate>\d+)[kK]_\d+', definition['videoUrl']) | |
138 | if mobj: | |
139 | if not height: | |
140 | height = int(mobj.group('height')) | |
141 | bitrate = int(mobj.group('bitrate')) | |
142 | f.update({ | |
143 | 'format_id': f'{height}p-{bitrate}k', | |
144 | 'tbr': bitrate, | |
145 | }) | |
146 | f['height'] = height | |
147 | formats.append(f) | |
148 | ||
149 | title = self._html_search_regex( | |
150 | r'(?s)<div[^>]+class=["\']watchVideoTitle[^>]+>(.+?)</div>', | |
151 | webpage, 'title', default=None) or self._og_search_title( | |
152 | webpage, default=None) or self._html_search_meta( | |
153 | 'title', webpage, fatal=True) | |
154 | ||
155 | description = self._html_search_regex( | |
156 | r'(?s)<div[^>]+\bid=["\']description["\'][^>]*>(.+?)</div>', | |
157 | webpage, 'description', | |
158 | default=None) or self._og_search_description( | |
159 | webpage, default=None) | |
160 | thumbnail = self._search_regex( | |
161 | r'(?:imageurl\s*=|poster\s*:)\s*(["\'])(?P<thumbnail>.+?)\1', | |
162 | webpage, 'thumbnail', fatal=False, group='thumbnail') | |
163 | duration = traverse_obj(player_vars, ('duration', {int_or_none})) | |
164 | if duration is None: | |
165 | duration = int_or_none(self._html_search_meta( | |
166 | 'video:duration', webpage, 'duration', fatal=False)) | |
167 | ||
168 | uploader = self._html_search_regex( | |
169 | r'(?s)<div[^>]+class=["\']submitByLink["\'][^>]*>(.+?)</div>', | |
170 | webpage, 'uploader', fatal=False) | |
171 | upload_date = unified_strdate(self._html_search_regex( | |
172 | (r'UPLOADED:\s*<span>([^<]+)', | |
173 | r'Date\s+[Aa]dded:\s*<span>([^<]+)', | |
174 | r'''(?s)<div[^>]+class=["']videoInfo(?:Date|Time)\b[^>]*>(.+?)</div>''', | |
175 | r'(?s)<label\b[^>]*>Uploaded[^<]*</label>\s*<span\b[^>]*>(.+?)</span>'), | |
176 | webpage, 'upload date', fatal=False)) | |
177 | ||
178 | age_limit = self._rta_search(webpage) | |
179 | ||
180 | view_count = None | |
181 | views = self._search_regex( | |
182 | r'(<div [^>]*\bdata-value\s*=[^>]+>)\s*<label>Views:</label>', | |
183 | webpage, 'views', default=None) | |
184 | if views: | |
185 | view_count = parse_count(extract_attributes(views).get('data-value')) | |
186 | comment_count = parse_count(self._search_regex( | |
187 | r'>All [Cc]omments? \(([\d,.]+)\)', | |
188 | webpage, 'comment count', default=None)) | |
189 | ||
190 | def extract_tag_box(regex, title): | |
191 | tag_box = self._search_regex(regex, webpage, title, default=None) | |
192 | if not tag_box: | |
193 | return [] | |
194 | return re.findall(r'<a[^>]+href=[^>]+>([^<]+)', tag_box) | |
195 | ||
196 | categories = extract_tag_box( | |
197 | r'(?s)Categories:.*?</[^>]+>(.+?)</div>', 'categories') | |
198 | tags = extract_tag_box( | |
199 | r'(?s)Tags:.*?</div>\s*<div[^>]+class=["\']tagBoxContent["\'][^>]*>(.+?)</div>', | |
200 | 'tags') | |
201 | ||
202 | data = self._search_json_ld(webpage, video_id, expected_type='VideoObject', fatal=False) | |
203 | data.pop('url', None) | |
204 | ||
205 | result = merge_dicts(data, { | |
206 | 'id': video_id, | |
207 | 'display_id': display_id, | |
208 | 'title': title, | |
209 | 'description': description, | |
210 | 'thumbnail': thumbnail, | |
211 | 'duration': duration, | |
212 | 'uploader': uploader, | |
213 | 'upload_date': upload_date, | |
214 | 'view_count': view_count, | |
215 | 'comment_count': comment_count, | |
216 | 'categories': categories, | |
217 | 'tags': tags, | |
218 | 'age_limit': age_limit, | |
219 | 'formats': formats, | |
220 | }) | |
221 | ||
222 | # Remove SEO spam "description" | |
223 | description = result.get('description') | |
224 | if description and description.startswith(f'Watch {result.get("title")} online'): | |
225 | del result['description'] | |
226 | ||
227 | return result | |
228 | ||
229 | ||
230 | class YouPornListBase(InfoExtractor): | |
231 | def _get_next_url(self, url, pl_id, html): | |
232 | return urljoin(url, self._search_regex( | |
233 | r'''<a [^>]*?\bhref\s*=\s*("|')(?P<url>(?:(?!\1)[^>])+)\1''', | |
234 | get_element_by_id('next', html) or '', 'next page', | |
235 | group='url', default=None)) | |
236 | ||
237 | @classmethod | |
238 | def _get_title_from_slug(cls, title_slug): | |
239 | return re.sub(r'[_-]', ' ', title_slug) | |
240 | ||
241 | def _entries(self, url, pl_id, html=None, page_num=None): | |
242 | start = page_num or 1 | |
243 | for page in itertools.count(start): | |
244 | if not html: | |
245 | html = self._download_webpage( | |
246 | url, pl_id, note=f'Downloading page {page}', fatal=page == start) | |
247 | if not html: | |
248 | return | |
249 | for element in get_elements_html_by_class('video-title', html): | |
250 | if video_url := traverse_obj(element, ({extract_attributes}, 'href', {lambda x: urljoin(url, x)})): | |
251 | yield self.url_result(video_url) | |
252 | ||
253 | if page_num is not None: | |
254 | return | |
255 | next_url = self._get_next_url(url, pl_id, html) | |
256 | if not next_url or next_url == url: | |
257 | return | |
258 | url = next_url | |
259 | html = None | |
260 | ||
261 | def _real_extract(self, url, html=None): | |
262 | m_dict = self._match_valid_url(url).groupdict() | |
263 | pl_id, page_type, sort = (m_dict.get(k) for k in ('id', 'type', 'sort')) | |
264 | qs = {k: v[-1] for k, v in parse_qs(url).items() if v} | |
265 | ||
266 | base_id = pl_id or 'YouPorn' | |
267 | title = self._get_title_from_slug(base_id) | |
268 | if page_type: | |
269 | title = f'{page_type.capitalize()} {title}' | |
270 | base_id = [base_id.lower()] | |
271 | if sort is None: | |
272 | title += ' videos' | |
273 | else: | |
274 | title = f'{title} videos by {re.sub(r"[_-]", " ", sort)}' | |
275 | base_id.append(sort) | |
276 | if qs: | |
277 | filters = list(map('='.join, sorted(qs.items()))) | |
278 | title += f' ({",".join(filters)})' | |
279 | base_id.extend(filters) | |
280 | pl_id = '/'.join(base_id) | |
281 | ||
282 | return self.playlist_result( | |
283 | self._entries(url, pl_id, html=html, page_num=int_or_none(qs.get('page'))), | |
284 | playlist_id=pl_id, playlist_title=title) | |
285 | ||
286 | ||
287 | class YouPornCategoryIE(YouPornListBase): | |
288 | IE_DESC = 'YouPorn category, with sorting, filtering and pagination' | |
289 | _VALID_URL = r'''(?x) | |
290 | https?://(?:www\.)?youporn\.com/ | |
291 | (?P<type>category)/(?P<id>[^/?#&]+) | |
292 | (?:/(?P<sort>popular|views|rating|time|duration))?/?(?:[#?]|$) | |
293 | ''' | |
294 | _TESTS = [{ | |
295 | 'note': 'Full list with pagination', | |
296 | 'url': 'https://www.youporn.com/category/popular-with-women/popular/', | |
297 | 'info_dict': { | |
298 | 'id': 'popular-with-women/popular', | |
299 | 'title': 'Category popular with women videos by popular', | |
300 | }, | |
301 | 'playlist_mincount': 39, | |
302 | }, { | |
303 | 'note': 'Filtered paginated list with single page result', | |
304 | 'url': 'https://www.youporn.com/category/popular-with-women/duration/?min_minutes=10', | |
305 | 'info_dict': { | |
306 | 'id': 'popular-with-women/duration/min_minutes=10', | |
307 | 'title': 'Category popular with women videos by duration (min_minutes=10)', | |
308 | }, | |
309 | 'playlist_mincount': 2, | |
310 | # 'playlist_maxcount': 30, | |
311 | }, { | |
312 | 'note': 'Single page of full list', | |
313 | 'url': 'https://www.youporn.com/category/popular-with-women/popular?page=1', | |
314 | 'info_dict': { | |
315 | 'id': 'popular-with-women/popular/page=1', | |
316 | 'title': 'Category popular with women videos by popular (page=1)', | |
317 | }, | |
318 | 'playlist_count': 36, | |
319 | }] | |
320 | ||
321 | ||
322 | class YouPornChannelIE(YouPornListBase): | |
323 | IE_DESC = 'YouPorn channel, with sorting and pagination' | |
324 | _VALID_URL = r'''(?x) | |
325 | https?://(?:www\.)?youporn\.com/ | |
326 | (?P<type>channel)/(?P<id>[^/?#&]+) | |
327 | (?:/(?P<sort>rating|views|duration))?/?(?:[#?]|$) | |
328 | ''' | |
329 | _TESTS = [{ | |
330 | 'note': 'Full list with pagination', | |
331 | 'url': 'https://www.youporn.com/channel/x-feeds/', | |
332 | 'info_dict': { | |
333 | 'id': 'x-feeds', | |
334 | 'title': 'Channel X-Feeds videos', | |
335 | }, | |
336 | 'playlist_mincount': 37, | |
337 | }, { | |
338 | 'note': 'Single page of full list (no filters here)', | |
339 | 'url': 'https://www.youporn.com/channel/x-feeds/duration?page=1', | |
340 | 'info_dict': { | |
341 | 'id': 'x-feeds/duration/page=1', | |
342 | 'title': 'Channel X-Feeds videos by duration (page=1)', | |
343 | }, | |
344 | 'playlist_count': 24, | |
345 | }] | |
346 | ||
347 | @staticmethod | |
348 | def _get_title_from_slug(title_slug): | |
349 | return re.sub(r'_', ' ', title_slug).title() | |
350 | ||
351 | ||
352 | class YouPornCollectionIE(YouPornListBase): | |
353 | IE_DESC = 'YouPorn collection (user playlist), with sorting and pagination' | |
354 | _VALID_URL = r'''(?x) | |
355 | https?://(?:www\.)?youporn\.com/ | |
356 | (?P<type>collection)s/videos/(?P<id>\d+) | |
357 | (?:/(?P<sort>rating|views|time|duration))?/?(?:[#?]|$) | |
358 | ''' | |
359 | _TESTS = [{ | |
360 | 'note': 'Full list with pagination', | |
361 | 'url': 'https://www.youporn.com/collections/videos/33044251/', | |
362 | 'info_dict': { | |
363 | 'id': '33044251', | |
364 | 'title': 'Collection Sexy Lips videos', | |
365 | 'uploader': 'ph-littlewillyb', | |
366 | }, | |
367 | 'playlist_mincount': 50, | |
368 | }, { | |
369 | 'note': 'Single page of full list (no filters here)', | |
370 | 'url': 'https://www.youporn.com/collections/videos/33044251/time?page=1', | |
371 | 'info_dict': { | |
372 | 'id': '33044251/time/page=1', | |
373 | 'title': 'Collection Sexy Lips videos by time (page=1)', | |
374 | 'uploader': 'ph-littlewillyb', | |
375 | }, | |
376 | 'playlist_count': 20, | |
377 | }] | |
378 | ||
379 | def _real_extract(self, url): | |
380 | pl_id = self._match_id(url) | |
381 | html = self._download_webpage(url, pl_id) | |
382 | playlist = super()._real_extract(url, html=html) | |
383 | infos = re.sub(r'\s+', ' ', clean_html(get_element_by_class( | |
384 | 'collection-infos', html)) or '') | |
385 | title, uploader = self._search_regex( | |
386 | r'^\s*Collection: (?P<title>.+?) \d+ VIDEOS \d+ VIEWS \d+ days LAST UPDATED From: (?P<uploader>[\w_-]+)', | |
387 | infos, 'title/uploader', group=('title', 'uploader'), default=(None, None)) | |
388 | if title: | |
389 | playlist.update({ | |
390 | 'title': playlist['title'].replace(playlist['id'].split('/')[0], title), | |
391 | 'uploader': uploader, | |
392 | }) | |
393 | ||
394 | return playlist | |
395 | ||
396 | ||
397 | class YouPornTagIE(YouPornListBase): | |
398 | IE_DESC = 'YouPorn tag (porntags), with sorting, filtering and pagination' | |
399 | _VALID_URL = r'''(?x) | |
400 | https?://(?:www\.)?youporn\.com/ | |
401 | porn(?P<type>tag)s/(?P<id>[^/?#&]+) | |
402 | (?:/(?P<sort>views|rating|time|duration))?/?(?:[#?]|$) | |
403 | ''' | |
404 | _TESTS = [{ | |
405 | 'note': 'Full list with pagination', | |
406 | 'url': 'https://www.youporn.com/porntags/austrian', | |
407 | 'info_dict': { | |
408 | 'id': 'austrian', | |
409 | 'title': 'Tag austrian videos', | |
410 | }, | |
411 | 'playlist_mincount': 33, | |
412 | 'expected_warnings': ['YouPorn tag pages are not correctly cached'], | |
413 | }, { | |
414 | 'note': 'Filtered paginated list with single page result', | |
415 | 'url': 'https://www.youporn.com/porntags/austrian/duration/?min_minutes=10', | |
416 | 'info_dict': { | |
417 | 'id': 'austrian/duration/min_minutes=10', | |
418 | 'title': 'Tag austrian videos by duration (min_minutes=10)', | |
419 | }, | |
420 | 'playlist_mincount': 10, | |
421 | # number of videos per page is (row x col) 2x3 + 6x4 + 2, or + 3, | |
422 | # or more, varying with number of ads; let's set max as 9x4 | |
423 | # NB col 1 may not be shown in non-JS page with site CSS and zoom 100% | |
424 | # 'playlist_maxcount': 32, | |
425 | 'expected_warnings': ['YouPorn tag pages are not correctly cached'], | |
426 | }, { | |
427 | 'note': 'Single page of full list', | |
428 | 'url': 'https://www.youporn.com/porntags/austrian/?page=1', | |
429 | 'info_dict': { | |
430 | 'id': 'austrian/page=1', | |
431 | 'title': 'Tag austrian videos (page=1)', | |
432 | }, | |
433 | 'playlist_mincount': 32, | |
434 | # 'playlist_maxcount': 34, | |
435 | 'expected_warnings': ['YouPorn tag pages are not correctly cached'], | |
436 | }] | |
437 | ||
438 | def _real_extract(self, url): | |
439 | self.report_warning( | |
440 | 'YouPorn tag pages are not correctly cached and ' | |
441 | 'often return incorrect results', only_once=True) | |
442 | return super()._real_extract(url) | |
443 | ||
444 | ||
445 | class YouPornStarIE(YouPornListBase): | |
446 | IE_DESC = 'YouPorn Pornstar, with description, sorting and pagination' | |
447 | _VALID_URL = r'''(?x) | |
448 | https?://(?:www\.)?youporn\.com/ | |
449 | (?P<type>pornstar)/(?P<id>[^/?#&]+) | |
450 | (?:/(?P<sort>rating|views|duration))?/?(?:[#?]|$) | |
451 | ''' | |
452 | _TESTS = [{ | |
453 | 'note': 'Full list with pagination', | |
454 | 'url': 'https://www.youporn.com/pornstar/daynia/', | |
455 | 'info_dict': { | |
456 | 'id': 'daynia', | |
457 | 'title': 'Pornstar Daynia videos', | |
458 | 'description': r're:Daynia Rank \d+ Videos \d+ Views [\d,.]+ .+ Subscribers \d+', | |
459 | }, | |
460 | 'playlist_mincount': 40, | |
461 | }, { | |
462 | 'note': 'Single page of full list (no filters here)', | |
463 | 'url': 'https://www.youporn.com/pornstar/daynia/?page=1', | |
464 | 'info_dict': { | |
465 | 'id': 'daynia/page=1', | |
466 | 'title': 'Pornstar Daynia videos (page=1)', | |
467 | 'description': 're:.{180,}', | |
468 | }, | |
469 | 'playlist_count': 26, | |
470 | }] | |
471 | ||
472 | @staticmethod | |
473 | def _get_title_from_slug(title_slug): | |
474 | return re.sub(r'_', ' ', title_slug).title() | |
475 | ||
476 | def _real_extract(self, url): | |
477 | pl_id = self._match_id(url) | |
478 | html = self._download_webpage(url, pl_id) | |
479 | playlist = super()._real_extract(url, html=html) | |
480 | INFO_ELEMENT_RE = r'''(?x) | |
481 | <div [^>]*\bclass\s*=\s*('|")(?:[\w$-]+\s+|\s)*?pornstar-info-wrapper(?:\s+[\w$-]+|\s)*\1[^>]*> | |
482 | (?P<info>[\s\S]+?)(?:</div>\s*){6,} | |
483 | ''' | |
484 | ||
485 | if infos := self._search_regex(INFO_ELEMENT_RE, html, 'infos', group='info', default=''): | |
486 | infos = re.sub( | |
487 | r'(?:\s*nl=nl)+\s*', ' ', | |
488 | re.sub(r'(?u)\s+', ' ', clean_html(re.sub('\n', 'nl=nl', infos)))).replace('ribe Subsc', '') | |
489 | ||
490 | return { | |
491 | **playlist, | |
492 | 'description': infos.strip() or None, | |
493 | } | |
494 | ||
495 | ||
496 | class YouPornVideosIE(YouPornListBase): | |
497 | IE_DESC = 'YouPorn video (browse) playlists, with sorting, filtering and pagination' | |
498 | _VALID_URL = r'''(?x) | |
499 | https?://(?:www\.)?youporn\.com/ | |
500 | (?:(?P<id>browse)/)? | |
501 | (?P<sort>(?(id) | |
502 | (?:duration|rating|time|views)| | |
503 | (?:most_(?:favou?rit|view)ed|recommended|top_rated)?)) | |
504 | (?:[/#?]|$) | |
505 | ''' | |
506 | _TESTS = [{ | |
507 | 'note': 'Full list with pagination (too long for test)', | |
508 | 'url': 'https://www.youporn.com/', | |
509 | 'info_dict': { | |
510 | 'id': 'youporn', | |
511 | 'title': 'YouPorn videos', | |
512 | }, | |
513 | 'only_matching': True, | |
514 | }, { | |
515 | 'note': 'Full list with pagination (too long for test)', | |
516 | 'url': 'https://www.youporn.com/recommended', | |
517 | 'info_dict': { | |
518 | 'id': 'youporn/recommended', | |
519 | 'title': 'YouPorn videos by recommended', | |
520 | }, | |
521 | 'only_matching': True, | |
522 | }, { | |
523 | 'note': 'Full list with pagination (too long for test)', | |
524 | 'url': 'https://www.youporn.com/top_rated', | |
525 | 'info_dict': { | |
526 | 'id': 'youporn/top_rated', | |
527 | 'title': 'YouPorn videos by top rated', | |
528 | }, | |
529 | 'only_matching': True, | |
530 | }, { | |
531 | 'note': 'Full list with pagination (too long for test)', | |
532 | 'url': 'https://www.youporn.com/browse/time', | |
533 | 'info_dict': { | |
534 | 'id': 'browse/time', | |
535 | 'title': 'YouPorn videos by time', | |
536 | }, | |
537 | 'only_matching': True, | |
538 | }, { | |
539 | 'note': 'Filtered paginated list with single page result', | |
540 | 'url': 'https://www.youporn.com/most_favorited/?res=VR&max_minutes=2', | |
541 | 'info_dict': { | |
542 | 'id': 'youporn/most_favorited/max_minutes=2/res=VR', | |
543 | 'title': 'YouPorn videos by most favorited (max_minutes=2,res=VR)', | |
544 | }, | |
545 | 'playlist_mincount': 10, | |
546 | # 'playlist_maxcount': 28, | |
547 | }, { | |
548 | 'note': 'Filtered paginated list with several pages', | |
549 | 'url': 'https://www.youporn.com/most_favorited/?res=VR&max_minutes=5', | |
550 | 'info_dict': { | |
551 | 'id': 'youporn/most_favorited/max_minutes=5/res=VR', | |
552 | 'title': 'YouPorn videos by most favorited (max_minutes=5,res=VR)', | |
553 | }, | |
554 | 'playlist_mincount': 45, | |
555 | }, { | |
556 | 'note': 'Single page of full list', | |
557 | 'url': 'https://www.youporn.com/browse/time?page=1', | |
558 | 'info_dict': { | |
559 | 'id': 'browse/time/page=1', | |
560 | 'title': 'YouPorn videos by time (page=1)', | |
561 | }, | |
562 | 'playlist_count': 36, | |
563 | }] | |
564 | ||
565 | @staticmethod | |
566 | def _get_title_from_slug(title_slug): | |
567 | return 'YouPorn' if title_slug == 'browse' else title_slug |