]>
Commit | Line | Data |
---|---|---|
1 | # coding: utf-8 | |
2 | from __future__ import unicode_literals | |
3 | ||
4 | import itertools | |
5 | ||
6 | from .common import InfoExtractor | |
7 | from .vimeo import VimeoIE | |
8 | ||
9 | from ..compat import compat_urllib_parse_unquote | |
10 | from ..utils import ( | |
11 | clean_html, | |
12 | determine_ext, | |
13 | int_or_none, | |
14 | KNOWN_EXTENSIONS, | |
15 | mimetype2ext, | |
16 | parse_iso8601, | |
17 | str_or_none, | |
18 | try_get, | |
19 | url_or_none, | |
20 | ) | |
21 | ||
22 | ||
23 | class PatreonIE(InfoExtractor): | |
24 | _VALID_URL = r'https?://(?:www\.)?patreon\.com/(?:creation\?hid=|posts/(?:[\w-]+-)?)(?P<id>\d+)' | |
25 | _TESTS = [{ | |
26 | 'url': 'http://www.patreon.com/creation?hid=743933', | |
27 | 'md5': 'e25505eec1053a6e6813b8ed369875cc', | |
28 | 'info_dict': { | |
29 | 'id': '743933', | |
30 | 'ext': 'mp3', | |
31 | 'title': 'Episode 166: David Smalley of Dogma Debate', | |
32 | 'description': 'md5:713b08b772cd6271b9f3906683cfacdf', | |
33 | 'uploader': 'Cognitive Dissonance Podcast', | |
34 | 'thumbnail': 're:^https?://.*$', | |
35 | 'timestamp': 1406473987, | |
36 | 'upload_date': '20140727', | |
37 | 'uploader_id': '87145', | |
38 | }, | |
39 | }, { | |
40 | 'url': 'http://www.patreon.com/creation?hid=754133', | |
41 | 'md5': '3eb09345bf44bf60451b8b0b81759d0a', | |
42 | 'info_dict': { | |
43 | 'id': '754133', | |
44 | 'ext': 'mp3', | |
45 | 'title': 'CD 167 Extra', | |
46 | 'uploader': 'Cognitive Dissonance Podcast', | |
47 | 'thumbnail': 're:^https?://.*$', | |
48 | }, | |
49 | 'skip': 'Patron-only content', | |
50 | }, { | |
51 | 'url': 'https://www.patreon.com/creation?hid=1682498', | |
52 | 'info_dict': { | |
53 | 'id': 'SU4fj_aEMVw', | |
54 | 'ext': 'mp4', | |
55 | 'title': 'I\'m on Patreon!', | |
56 | 'uploader': 'TraciJHines', | |
57 | 'thumbnail': 're:^https?://.*$', | |
58 | 'upload_date': '20150211', | |
59 | 'description': 'md5:c5a706b1f687817a3de09db1eb93acd4', | |
60 | 'uploader_id': 'TraciJHines', | |
61 | }, | |
62 | 'params': { | |
63 | 'noplaylist': True, | |
64 | 'skip_download': True, | |
65 | } | |
66 | }, { | |
67 | 'url': 'https://www.patreon.com/posts/episode-166-of-743933', | |
68 | 'only_matching': True, | |
69 | }, { | |
70 | 'url': 'https://www.patreon.com/posts/743933', | |
71 | 'only_matching': True, | |
72 | }, { | |
73 | 'url': 'https://www.patreon.com/posts/kitchen-as-seen-51706779', | |
74 | 'md5': '96656690071f6d64895866008484251b', | |
75 | 'info_dict': { | |
76 | 'id': '555089736', | |
77 | 'ext': 'mp4', | |
78 | 'title': 'KITCHEN AS SEEN ON DEEZ NUTS EXTENDED!', | |
79 | 'uploader': 'Cold Ones', | |
80 | 'thumbnail': 're:^https?://.*$', | |
81 | 'upload_date': '20210526', | |
82 | 'description': 'md5:557a409bd79d3898689419094934ba79', | |
83 | 'uploader_id': '14936315', | |
84 | }, | |
85 | 'skip': 'Patron-only content' | |
86 | }] | |
87 | ||
88 | # Currently Patreon exposes download URL via hidden CSS, so login is not | |
89 | # needed. Keeping this commented for when this inevitably changes. | |
90 | ''' | |
91 | def _perform_login(self, username, password): | |
92 | login_form = { | |
93 | 'redirectUrl': 'http://www.patreon.com/', | |
94 | 'email': username, | |
95 | 'password': password, | |
96 | } | |
97 | ||
98 | request = sanitized_Request( | |
99 | 'https://www.patreon.com/processLogin', | |
100 | compat_urllib_parse_urlencode(login_form).encode('utf-8') | |
101 | ) | |
102 | login_page = self._download_webpage(request, None, note='Logging in') | |
103 | ||
104 | if re.search(r'onLoginFailed', login_page): | |
105 | raise ExtractorError('Unable to login, incorrect username and/or password', expected=True) | |
106 | ||
107 | ''' | |
108 | ||
109 | def _real_extract(self, url): | |
110 | video_id = self._match_id(url) | |
111 | post = self._download_json( | |
112 | 'https://www.patreon.com/api/posts/' + video_id, video_id, query={ | |
113 | 'fields[media]': 'download_url,mimetype,size_bytes', | |
114 | 'fields[post]': 'comment_count,content,embed,image,like_count,post_file,published_at,title', | |
115 | 'fields[user]': 'full_name,url', | |
116 | 'json-api-use-default-includes': 'false', | |
117 | 'include': 'media,user', | |
118 | }) | |
119 | attributes = post['data']['attributes'] | |
120 | title = attributes['title'].strip() | |
121 | image = attributes.get('image') or {} | |
122 | info = { | |
123 | 'id': video_id, | |
124 | 'title': title, | |
125 | 'description': clean_html(attributes.get('content')), | |
126 | 'thumbnail': image.get('large_url') or image.get('url'), | |
127 | 'timestamp': parse_iso8601(attributes.get('published_at')), | |
128 | 'like_count': int_or_none(attributes.get('like_count')), | |
129 | 'comment_count': int_or_none(attributes.get('comment_count')), | |
130 | } | |
131 | ||
132 | for i in post.get('included', []): | |
133 | i_type = i.get('type') | |
134 | if i_type == 'media': | |
135 | media_attributes = i.get('attributes') or {} | |
136 | download_url = media_attributes.get('download_url') | |
137 | ext = mimetype2ext(media_attributes.get('mimetype')) | |
138 | if download_url and ext in KNOWN_EXTENSIONS: | |
139 | info.update({ | |
140 | 'ext': ext, | |
141 | 'filesize': int_or_none(media_attributes.get('size_bytes')), | |
142 | 'url': download_url, | |
143 | }) | |
144 | elif i_type == 'user': | |
145 | user_attributes = i.get('attributes') | |
146 | if user_attributes: | |
147 | info.update({ | |
148 | 'uploader': user_attributes.get('full_name'), | |
149 | 'uploader_id': str_or_none(i.get('id')), | |
150 | 'uploader_url': user_attributes.get('url'), | |
151 | }) | |
152 | ||
153 | if not info.get('url'): | |
154 | # handle Vimeo embeds | |
155 | if try_get(attributes, lambda x: x['embed']['provider']) == 'Vimeo': | |
156 | embed_html = try_get(attributes, lambda x: x['embed']['html']) | |
157 | v_url = url_or_none(compat_urllib_parse_unquote( | |
158 | self._search_regex(r'(https(?:%3A%2F%2F|://)player\.vimeo\.com.+app_id(?:=|%3D)+\d+)', embed_html, 'vimeo url', fatal=False))) | |
159 | if v_url: | |
160 | info.update({ | |
161 | '_type': 'url_transparent', | |
162 | 'url': VimeoIE._smuggle_referrer(v_url, 'https://patreon.com'), | |
163 | 'ie_key': 'Vimeo', | |
164 | }) | |
165 | ||
166 | if not info.get('url'): | |
167 | embed_url = try_get(attributes, lambda x: x['embed']['url']) | |
168 | if embed_url: | |
169 | info.update({ | |
170 | '_type': 'url', | |
171 | 'url': embed_url, | |
172 | }) | |
173 | ||
174 | if not info.get('url'): | |
175 | post_file = attributes['post_file'] | |
176 | ext = determine_ext(post_file.get('name')) | |
177 | if ext in KNOWN_EXTENSIONS: | |
178 | info.update({ | |
179 | 'ext': ext, | |
180 | 'url': post_file['url'], | |
181 | }) | |
182 | ||
183 | return info | |
184 | ||
185 | ||
186 | class PatreonUserIE(InfoExtractor): | |
187 | ||
188 | _VALID_URL = r'https?://(?:www\.)?patreon\.com/(?!rss)(?P<id>[-\w]+)' | |
189 | ||
190 | _TESTS = [{ | |
191 | 'url': 'https://www.patreon.com/dissonancepod/', | |
192 | 'info_dict': { | |
193 | 'title': 'dissonancepod', | |
194 | }, | |
195 | 'playlist_mincount': 68, | |
196 | 'expected_warnings': 'Post not viewable by current user! Skipping!', | |
197 | }, { | |
198 | 'url': 'https://www.patreon.com/dissonancepod/posts', | |
199 | 'only_matching': True | |
200 | }, ] | |
201 | ||
202 | @classmethod | |
203 | def suitable(cls, url): | |
204 | return False if PatreonIE.suitable(url) else super(PatreonUserIE, cls).suitable(url) | |
205 | ||
206 | def _entries(self, campaign_id, user_id): | |
207 | cursor = None | |
208 | params = { | |
209 | 'fields[campaign]': 'show_audio_post_download_links,name,url', | |
210 | 'fields[post]': 'current_user_can_view,embed,image,is_paid,post_file,published_at,patreon_url,url,post_type,thumbnail_url,title', | |
211 | 'filter[campaign_id]': campaign_id, | |
212 | 'filter[is_draft]': 'false', | |
213 | 'sort': '-published_at', | |
214 | 'json-api-version': 1.0, | |
215 | 'json-api-use-default-includes': 'false', | |
216 | } | |
217 | ||
218 | for page in itertools.count(1): | |
219 | ||
220 | params.update({'page[cursor]': cursor} if cursor else {}) | |
221 | posts_json = self._download_json('https://www.patreon.com/api/posts', user_id, note='Downloading posts page %d' % page, query=params, headers={'Cookie': '.'}) | |
222 | ||
223 | cursor = try_get(posts_json, lambda x: x['meta']['pagination']['cursors']['next']) | |
224 | ||
225 | for post in posts_json.get('data') or []: | |
226 | yield self.url_result(url_or_none(try_get(post, lambda x: x['attributes']['patreon_url'])), 'Patreon') | |
227 | ||
228 | if cursor is None: | |
229 | break | |
230 | ||
231 | def _real_extract(self, url): | |
232 | ||
233 | user_id = self._match_id(url) | |
234 | webpage = self._download_webpage(url, user_id, headers={'Cookie': '.'}) | |
235 | campaign_id = self._search_regex(r'https://www.patreon.com/api/campaigns/(\d+)/?', webpage, 'Campaign ID') | |
236 | return self.playlist_result(self._entries(campaign_id, user_id), playlist_title=user_id) |