]>
Commit | Line | Data |
---|---|---|
bf57cfa8 | 1 | import functools |
8c188d5d | 2 | import urllib |
4e4ba1d7 | 3 | |
4 | from .common import InfoExtractor | |
bf57cfa8 | 5 | from ..compat import compat_parse_qs |
4e4ba1d7 | 6 | from ..utils import ( |
7 | ExtractorError, | |
8 | int_or_none, | |
9 | qualities, | |
10 | try_get, | |
bf57cfa8 | 11 | OnDemandPagedList, |
4e4ba1d7 | 12 | ) |
13 | ||
14 | ||
bf57cfa8 | 15 | class RedGifsBaseInfoExtractor(InfoExtractor): |
4e4ba1d7 | 16 | _FORMATS = { |
17 | 'gif': 250, | |
18 | 'sd': 480, | |
19 | 'hd': None, | |
20 | } | |
bf57cfa8 | 21 | |
c53e5cf5 | 22 | _API_HEADERS = { |
23 | 'referer': 'https://www.redgifs.com/', | |
24 | 'origin': 'https://www.redgifs.com', | |
25 | 'content-type': 'application/json', | |
26 | } | |
27 | ||
bf57cfa8 DS |
28 | def _parse_gif_data(self, gif_data): |
29 | video_id = gif_data.get('id') | |
30 | quality = qualities(tuple(self._FORMATS.keys())) | |
31 | ||
32 | orig_height = int_or_none(gif_data.get('height')) | |
33 | aspect_ratio = try_get(gif_data, lambda x: orig_height / x['width']) | |
34 | ||
35 | formats = [] | |
36 | for format_id, height in self._FORMATS.items(): | |
37 | video_url = gif_data['urls'].get(format_id) | |
38 | if not video_url: | |
39 | continue | |
40 | height = min(orig_height, height or orig_height) | |
41 | formats.append({ | |
42 | 'url': video_url, | |
43 | 'format_id': format_id, | |
44 | 'width': height * aspect_ratio if aspect_ratio else None, | |
45 | 'height': height, | |
46 | 'quality': quality(format_id), | |
47 | }) | |
48 | self._sort_formats(formats) | |
49 | ||
50 | return { | |
51 | 'id': video_id, | |
52 | 'webpage_url': f'https://redgifs.com/watch/{video_id}', | |
c53e5cf5 | 53 | 'extractor_key': RedGifsIE.ie_key(), |
bf57cfa8 DS |
54 | 'extractor': 'RedGifs', |
55 | 'title': ' '.join(gif_data.get('tags') or []) or 'RedGifs', | |
56 | 'timestamp': int_or_none(gif_data.get('createDate')), | |
57 | 'uploader': gif_data.get('userName'), | |
58 | 'duration': int_or_none(gif_data.get('duration')), | |
59 | 'view_count': int_or_none(gif_data.get('views')), | |
60 | 'like_count': int_or_none(gif_data.get('likes')), | |
61 | 'categories': gif_data.get('tags') or [], | |
62 | 'tags': gif_data.get('tags'), | |
63 | 'age_limit': 18, | |
64 | 'formats': formats, | |
65 | } | |
66 | ||
c53e5cf5 | 67 | def _fetch_oauth_token(self, video_id): |
0c908911 | 68 | # https://github.com/Redgifs/api/wiki/Temporary-tokens |
69 | auth = self._download_json('https://api.redgifs.com/v2/auth/temporary', | |
70 | video_id, note='Fetching temporary token') | |
71 | if not auth.get('token'): | |
72 | raise ExtractorError('Unable to get temporary token') | |
73 | self._API_HEADERS['authorization'] = f'Bearer {auth["token"]}' | |
c53e5cf5 | 74 | |
bf57cfa8 | 75 | def _call_api(self, ep, video_id, *args, **kwargs): |
8c188d5d KW |
76 | for attempt in range(2): |
77 | if 'authorization' not in self._API_HEADERS: | |
78 | self._fetch_oauth_token(video_id) | |
79 | try: | |
80 | headers = dict(self._API_HEADERS) | |
81 | headers['x-customheader'] = f'https://www.redgifs.com/watch/{video_id}' | |
82 | data = self._download_json( | |
83 | f'https://api.redgifs.com/v2/{ep}', video_id, headers=headers, *args, **kwargs) | |
84 | break | |
85 | except ExtractorError as e: | |
86 | if not attempt and isinstance(e.cause, urllib.error.HTTPError) and e.cause.code == 401: | |
87 | del self._API_HEADERS['authorization'] # refresh the token | |
88 | raise | |
c53e5cf5 | 89 | |
bf57cfa8 DS |
90 | if 'error' in data: |
91 | raise ExtractorError(f'RedGifs said: {data["error"]}', expected=True, video_id=video_id) | |
92 | return data | |
93 | ||
94 | def _fetch_page(self, ep, video_id, query, page): | |
95 | query['page'] = page + 1 | |
96 | data = self._call_api( | |
97 | ep, video_id, query=query, note=f'Downloading JSON metadata page {page + 1}') | |
98 | ||
99 | for entry in data['gifs']: | |
100 | yield self._parse_gif_data(entry) | |
101 | ||
102 | def _prepare_api_query(self, query, fields): | |
103 | api_query = [ | |
104 | (field_name, query.get(field_name, (default,))[0]) | |
105 | for field_name, default in fields.items()] | |
106 | ||
107 | return {key: val for key, val in api_query if val is not None} | |
108 | ||
109 | def _paged_entries(self, ep, item_id, query, fields): | |
110 | page = int_or_none(query.get('page', (None,))[0]) | |
111 | page_fetcher = functools.partial( | |
112 | self._fetch_page, ep, item_id, self._prepare_api_query(query, fields)) | |
113 | return page_fetcher(page) if page else OnDemandPagedList(page_fetcher, self._PAGE_SIZE) | |
114 | ||
115 | ||
116 | class RedGifsIE(RedGifsBaseInfoExtractor): | |
117 | _VALID_URL = r'https?://(?:(?:www\.)?redgifs\.com/watch/|thumbs2\.redgifs\.com/)(?P<id>[^-/?#\.]+)' | |
4e4ba1d7 | 118 | _TESTS = [{ |
119 | 'url': 'https://www.redgifs.com/watch/squeakyhelplesswisent', | |
120 | 'info_dict': { | |
121 | 'id': 'squeakyhelplesswisent', | |
122 | 'ext': 'mp4', | |
123 | 'title': 'Hotwife Legs Thick', | |
124 | 'timestamp': 1636287915, | |
125 | 'upload_date': '20211107', | |
126 | 'uploader': 'ignored52', | |
127 | 'duration': 16, | |
128 | 'view_count': int, | |
129 | 'like_count': int, | |
130 | 'categories': list, | |
131 | 'age_limit': 18, | |
c53e5cf5 | 132 | 'tags': list, |
4e4ba1d7 | 133 | } |
134 | }, { | |
135 | 'url': 'https://thumbs2.redgifs.com/SqueakyHelplessWisent-mobile.mp4#t=0', | |
136 | 'info_dict': { | |
137 | 'id': 'squeakyhelplesswisent', | |
138 | 'ext': 'mp4', | |
139 | 'title': 'Hotwife Legs Thick', | |
140 | 'timestamp': 1636287915, | |
141 | 'upload_date': '20211107', | |
142 | 'uploader': 'ignored52', | |
143 | 'duration': 16, | |
144 | 'view_count': int, | |
145 | 'like_count': int, | |
146 | 'categories': list, | |
147 | 'age_limit': 18, | |
c53e5cf5 | 148 | 'tags': list, |
4e4ba1d7 | 149 | } |
150 | }] | |
151 | ||
152 | def _real_extract(self, url): | |
153 | video_id = self._match_id(url).lower() | |
bf57cfa8 | 154 | video_info = self._call_api( |
c53e5cf5 | 155 | f'gifs/{video_id}?views=yes', video_id, note='Downloading video info') |
bf57cfa8 | 156 | return self._parse_gif_data(video_info['gif']) |
4e4ba1d7 | 157 | |
4e4ba1d7 | 158 | |
bf57cfa8 DS |
159 | class RedGifsSearchIE(RedGifsBaseInfoExtractor): |
160 | IE_DESC = 'Redgifs search' | |
161 | _VALID_URL = r'https?://(?:www\.)?redgifs\.com/browse\?(?P<query>[^#]+)' | |
162 | _PAGE_SIZE = 80 | |
163 | _TESTS = [ | |
164 | { | |
165 | 'url': 'https://www.redgifs.com/browse?tags=Lesbian', | |
166 | 'info_dict': { | |
167 | 'id': 'tags=Lesbian', | |
168 | 'title': 'Lesbian', | |
169 | 'description': 'RedGifs search for Lesbian, ordered by trending' | |
170 | }, | |
171 | 'playlist_mincount': 100, | |
172 | }, | |
173 | { | |
174 | 'url': 'https://www.redgifs.com/browse?type=g&order=latest&tags=Lesbian', | |
175 | 'info_dict': { | |
176 | 'id': 'type=g&order=latest&tags=Lesbian', | |
177 | 'title': 'Lesbian', | |
178 | 'description': 'RedGifs search for Lesbian, ordered by latest' | |
179 | }, | |
180 | 'playlist_mincount': 100, | |
181 | }, | |
182 | { | |
183 | 'url': 'https://www.redgifs.com/browse?type=g&order=latest&tags=Lesbian&page=2', | |
184 | 'info_dict': { | |
185 | 'id': 'type=g&order=latest&tags=Lesbian&page=2', | |
186 | 'title': 'Lesbian', | |
187 | 'description': 'RedGifs search for Lesbian, ordered by latest' | |
188 | }, | |
189 | 'playlist_count': 80, | |
190 | } | |
191 | ] | |
4e4ba1d7 | 192 | |
bf57cfa8 DS |
193 | def _real_extract(self, url): |
194 | query_str = self._match_valid_url(url).group('query') | |
195 | query = compat_parse_qs(query_str) | |
196 | if not query.get('tags'): | |
197 | raise ExtractorError('Invalid query tags', expected=True) | |
4e4ba1d7 | 198 | |
bf57cfa8 DS |
199 | tags = query.get('tags')[0] |
200 | order = query.get('order', ('trending',))[0] | |
4e4ba1d7 | 201 | |
bf57cfa8 DS |
202 | query['search_text'] = [tags] |
203 | entries = self._paged_entries('gifs/search', query_str, query, { | |
204 | 'search_text': None, | |
205 | 'order': 'trending', | |
206 | 'type': None, | |
207 | }) | |
4e4ba1d7 | 208 | |
bf57cfa8 DS |
209 | return self.playlist_result( |
210 | entries, query_str, tags, f'RedGifs search for {tags}, ordered by {order}') | |
211 | ||
212 | ||
213 | class RedGifsUserIE(RedGifsBaseInfoExtractor): | |
214 | IE_DESC = 'Redgifs user' | |
215 | _VALID_URL = r'https?://(?:www\.)?redgifs\.com/users/(?P<username>[^/?#]+)(?:\?(?P<query>[^#]+))?' | |
216 | _PAGE_SIZE = 30 | |
217 | _TESTS = [ | |
218 | { | |
219 | 'url': 'https://www.redgifs.com/users/lamsinka89', | |
220 | 'info_dict': { | |
221 | 'id': 'lamsinka89', | |
222 | 'title': 'lamsinka89', | |
223 | 'description': 'RedGifs user lamsinka89, ordered by recent' | |
224 | }, | |
225 | 'playlist_mincount': 100, | |
226 | }, | |
227 | { | |
228 | 'url': 'https://www.redgifs.com/users/lamsinka89?page=3', | |
229 | 'info_dict': { | |
230 | 'id': 'lamsinka89?page=3', | |
231 | 'title': 'lamsinka89', | |
232 | 'description': 'RedGifs user lamsinka89, ordered by recent' | |
233 | }, | |
234 | 'playlist_count': 30, | |
235 | }, | |
236 | { | |
237 | 'url': 'https://www.redgifs.com/users/lamsinka89?order=best&type=g', | |
238 | 'info_dict': { | |
239 | 'id': 'lamsinka89?order=best&type=g', | |
240 | 'title': 'lamsinka89', | |
241 | 'description': 'RedGifs user lamsinka89, ordered by best' | |
242 | }, | |
243 | 'playlist_mincount': 100, | |
4e4ba1d7 | 244 | } |
bf57cfa8 DS |
245 | ] |
246 | ||
247 | def _real_extract(self, url): | |
248 | username, query_str = self._match_valid_url(url).group('username', 'query') | |
249 | playlist_id = f'{username}?{query_str}' if query_str else username | |
250 | ||
251 | query = compat_parse_qs(query_str) | |
252 | order = query.get('order', ('recent',))[0] | |
253 | ||
254 | entries = self._paged_entries(f'users/{username}/search', playlist_id, query, { | |
255 | 'order': 'recent', | |
256 | 'type': None, | |
257 | }) | |
258 | ||
259 | return self.playlist_result( | |
260 | entries, playlist_id, username, f'RedGifs user {username}, ordered by {order}') |