@danhachuel/thunderbolt 0.6.58 → 0.6.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/integrations/instagram_public.py +63 -18
- package/package.json +1 -1
|
@@ -166,34 +166,68 @@ def _extract_posts_from_html(document: str, limit: int = 10) -> list[dict[str, A
|
|
|
166
166
|
return posts
|
|
167
167
|
|
|
168
168
|
|
|
169
|
+
def _metric_text_value(value: Any) -> int | None:
|
|
170
|
+
if isinstance(value, Mapping):
|
|
171
|
+
value = value.get('count') or value.get('value')
|
|
172
|
+
text = str(value or '').strip().casefold().replace(' ', '')
|
|
173
|
+
if not text:
|
|
174
|
+
return None
|
|
175
|
+
multiplier = 1
|
|
176
|
+
if text.endswith(('k', 'mil')):
|
|
177
|
+
multiplier = 1000
|
|
178
|
+
text = text[:-1] if text.endswith('k') else text[:-3]
|
|
179
|
+
elif text.endswith(('m', 'mi', 'milhões')):
|
|
180
|
+
multiplier = 1_000_000
|
|
181
|
+
text = text[:-1] if text.endswith('m') else text[:-2] if text.endswith('mi') else text[:-7]
|
|
182
|
+
digits = re.sub(r'[^0-9]', '', text)
|
|
183
|
+
if not digits:
|
|
184
|
+
return None
|
|
185
|
+
return int(digits) * multiplier
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _embedded_profile_user(document: str, username: str) -> Mapping[str, Any]:
|
|
189
|
+
wanted = username.casefold()
|
|
190
|
+
for payload in _embedded_json_documents(document):
|
|
191
|
+
for node in _walk_json(payload):
|
|
192
|
+
if not isinstance(node, Mapping):
|
|
193
|
+
continue
|
|
194
|
+
node_username = str(node.get('username') or node.get('handle') or '').lstrip('@').casefold()
|
|
195
|
+
if node_username == wanted and any(key in node for key in ('biography', 'bio', 'edge_followed_by', 'edge_follow', 'followers', 'following')):
|
|
196
|
+
return node
|
|
197
|
+
return {}
|
|
198
|
+
|
|
199
|
+
|
|
169
200
|
def _extract_profile_user_from_html(document: str, username: str) -> dict[str, Any]:
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
201
|
+
embedded_user = _embedded_profile_user(document, username)
|
|
202
|
+
seo_description = _meta(document, 'og:description')
|
|
203
|
+
biography = str(embedded_user.get('biography') or embedded_user.get('bio') or '').strip()
|
|
204
|
+
description = biography or seo_description
|
|
205
|
+
title = str(embedded_user.get('full_name') or embedded_user.get('name') or _meta(document, 'og:title') or username).split('(')[0].strip() or username
|
|
206
|
+
avatar_url = str(embedded_user.get('profile_pic_url_hd') or embedded_user.get('profile_pic_url') or _meta(document, 'og:image')).strip()
|
|
173
207
|
|
|
174
208
|
def metric(patterns: tuple[str, ...]) -> int | None:
|
|
175
209
|
for pattern in patterns:
|
|
176
|
-
found = re.search(pattern,
|
|
210
|
+
found = re.search(pattern, seo_description, flags=re.IGNORECASE)
|
|
177
211
|
if found:
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
pass
|
|
212
|
+
parsed = _metric_text_value(found.group(1) + (found.group(2) or ''))
|
|
213
|
+
if parsed is not None:
|
|
214
|
+
return parsed
|
|
182
215
|
return None
|
|
183
216
|
|
|
184
|
-
followers = metric((r'([\d
|
|
185
|
-
following = metric((r'([\d
|
|
186
|
-
post_count = metric((r'([\d
|
|
187
|
-
country = ''
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
217
|
+
followers = _metric_text_value(embedded_user.get('edge_followed_by')) or _metric_text_value(embedded_user.get('followers')) or metric((r'([\d.,]+)\s*(k|mil)?\s*(?:followers|seguidores)', r'(?:followers|seguidores)\s*([\d.,]+)\s*(k|mil)?')) or _structured_metric(document, 'edge_followed_by', 'followers', 'follower_count') or _structured_metric_from_json(document, 'edge_followed_by', 'followers', 'follower_count', 'followerCount')
|
|
218
|
+
following = _metric_text_value(embedded_user.get('edge_follow')) or _metric_text_value(embedded_user.get('following')) or metric((r'([\d.,]+)\s*(k|mil)?\s*(?:following|seguindo)', r'(?:following|seguindo)\s*([\d.,]+)\s*(k|mil)?')) or _structured_metric(document, 'edge_follow', 'follows', 'following', 'following_count', 'followingCount') or _structured_metric_from_json(document, 'edge_follow', 'follows', 'following', 'following_count', 'followingCount')
|
|
219
|
+
post_count = _metric_text_value(embedded_user.get('edge_owner_to_timeline_media')) or _metric_text_value(embedded_user.get('posts')) or metric((r'([\d.,]+)\s*(k|mil)?\s*(?:posts|publicações|publications)', r'(?:posts|publicações|publications)\s*([\d.,]+)\s*(k|mil)?')) or _structured_metric(document, 'edge_owner_to_timeline_media', 'posts', 'post_count')
|
|
220
|
+
country = _country_value(embedded_user.get('country') or embedded_user.get('country_code')) or extract_public_instagram_country(embedded_user)
|
|
221
|
+
if not country:
|
|
222
|
+
for payload in _embedded_json_documents(document):
|
|
223
|
+
country = extract_public_instagram_country(payload)
|
|
224
|
+
if country:
|
|
225
|
+
break
|
|
192
226
|
posts = _extract_posts_from_html(document, 10)
|
|
193
227
|
user: dict[str, Any] = {
|
|
194
228
|
'username': username,
|
|
195
229
|
'full_name': title.split('(')[0].strip() or username,
|
|
196
|
-
'biography':
|
|
230
|
+
'biography': biography,
|
|
197
231
|
'edge_followed_by': {'count': followers} if followers is not None else {},
|
|
198
232
|
'edge_follow': {'count': following} if following is not None else {},
|
|
199
233
|
'edge_owner_to_timeline_media': {'count': post_count, 'edges': [{'node': post} for post in posts]},
|
|
@@ -227,6 +261,15 @@ def _fetch_web_profile_user(username: str) -> dict[str, Any] | None:
|
|
|
227
261
|
merged[key] = value
|
|
228
262
|
return merged
|
|
229
263
|
|
|
264
|
+
def profile_is_complete(user: Mapping[str, Any]) -> bool:
|
|
265
|
+
if profile_quality(user) < 4:
|
|
266
|
+
return False
|
|
267
|
+
if platform.system() != 'Windows':
|
|
268
|
+
return True
|
|
269
|
+
has_bio = bool(str(user.get('biography') or user.get('bio') or '').strip())
|
|
270
|
+
has_following = any(user.get(key) not in (None, '', {}, []) for key in ('edge_follow', 'follows', 'following', 'following_count', 'followingCount'))
|
|
271
|
+
return has_bio and has_following
|
|
272
|
+
|
|
230
273
|
best_user: dict[str, Any] | None = None
|
|
231
274
|
for endpoint in endpoints:
|
|
232
275
|
try:
|
|
@@ -239,7 +282,7 @@ def _fetch_web_profile_user(username: str) -> dict[str, Any] | None:
|
|
|
239
282
|
user = ((payload.get('data') or {}).get('user') if isinstance(payload, dict) else None)
|
|
240
283
|
if isinstance(user, dict):
|
|
241
284
|
best_user = merge_profile(best_user, user)
|
|
242
|
-
if
|
|
285
|
+
if profile_is_complete(best_user):
|
|
243
286
|
return best_user
|
|
244
287
|
|
|
245
288
|
curl = shutil.which('curl')
|
|
@@ -419,6 +462,8 @@ def fetch_public_instagram_profile(source: str) -> IntegrationResult:
|
|
|
419
462
|
api_user = _fetch_web_profile_user(reference['username'])
|
|
420
463
|
if api_user:
|
|
421
464
|
return IntegrationResult(True, 'Perfil Instagram encontrado publicamente.', _profile_data_from_api(api_user, reference))
|
|
465
|
+
if platform.system() == 'Windows':
|
|
466
|
+
return IntegrationResult(False, 'Não foi possível encontrar o perfil público estruturado do Instagram.', reference)
|
|
422
467
|
try:
|
|
423
468
|
response = requests.get(reference['url'], headers={'User-Agent': 'Mozilla/5.0', 'Accept-Language': 'pt-BR,pt;q=0.9,en;q=0.8'}, timeout=12)
|
|
424
469
|
except requests.RequestException as exc:
|
package/package.json
CHANGED