@danhachuel/thunderbolt 0.6.59 → 0.6.61

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.
@@ -185,23 +185,45 @@ def _metric_text_value(value: Any) -> int | None:
185
185
  return int(digits) * multiplier
186
186
 
187
187
 
188
+ def _extract_bio_from_html(document: str) -> str:
189
+ """Extract only the JSON biography value; never use an SEO description as Bio."""
190
+ patterns = (
191
+ r'"biography"\s*:\s*"((?:\\.|[^"\\])*)"',
192
+ r"'biography'\s*:\s*'((?:\\.|[^'\\])*)'",
193
+ )
194
+ for pattern in patterns:
195
+ for match in re.finditer(pattern, document, flags=re.IGNORECASE | re.DOTALL):
196
+ raw_value = match.group(1)
197
+ try:
198
+ decoded = json.loads(f'"{raw_value}"')
199
+ except (TypeError, ValueError, json.JSONDecodeError):
200
+ decoded = bytes(raw_value, 'utf-8').decode('unicode_escape')
201
+ if str(decoded or '').strip():
202
+ return str(decoded).strip()
203
+ return ''
204
+
205
+
188
206
  def _embedded_profile_user(document: str, username: str) -> Mapping[str, Any]:
189
- wanted = username.casefold()
207
+ wanted = str(username or '').strip().lstrip('@').casefold()
190
208
  for payload in _embedded_json_documents(document):
191
209
  for node in _walk_json(payload):
192
210
  if not isinstance(node, Mapping):
193
211
  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')):
212
+ node_username = str(node.get('username') or node.get('handle') or '').strip().lstrip('@').casefold()
213
+ has_profile_fields = any(key in node for key in ('biography', 'bio', 'edge_followed_by', 'edge_follow', 'followers', 'following'))
214
+ if node_username == wanted and has_profile_fields:
196
215
  return node
197
216
  return {}
198
217
 
199
218
 
200
219
  def _extract_profile_user_from_html(document: str, username: str) -> dict[str, Any]:
201
220
  embedded_user = _embedded_profile_user(document, username)
221
+ regex_biography = _extract_bio_from_html(document)
202
222
  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
223
+ biography = str(embedded_user.get('biography') or embedded_user.get('bio') or regex_biography or '').strip()
224
+ print(f'embedded_user for @{username}: {embedded_user}')
225
+ print(f'biography extracted for @{username}: {biography!r}')
226
+ description = biography
205
227
  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
228
  avatar_url = str(embedded_user.get('profile_pic_url_hd') or embedded_user.get('profile_pic_url') or _meta(document, 'og:image')).strip()
207
229
 
@@ -214,9 +236,9 @@ def _extract_profile_user_from_html(document: str, username: str) -> dict[str, A
214
236
  return parsed
215
237
  return None
216
238
 
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)',)) 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)',)) 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)',)) or _structured_metric(document, 'edge_owner_to_timeline_media', 'posts', 'post_count')
239
+ 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')
240
+ 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')
241
+ 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
242
  country = _country_value(embedded_user.get('country') or embedded_user.get('country_code')) or extract_public_instagram_country(embedded_user)
221
243
  if not country:
222
244
  for payload in _embedded_json_documents(document):
@@ -227,7 +249,7 @@ def _extract_profile_user_from_html(document: str, username: str) -> dict[str, A
227
249
  user: dict[str, Any] = {
228
250
  'username': username,
229
251
  'full_name': title.split('(')[0].strip() or username,
230
- 'biography': description,
252
+ 'biography': biography,
231
253
  'edge_followed_by': {'count': followers} if followers is not None else {},
232
254
  'edge_follow': {'count': following} if following is not None else {},
233
255
  'edge_owner_to_timeline_media': {'count': post_count, 'edges': [{'node': post} for post in posts]},
@@ -235,7 +257,7 @@ def _extract_profile_user_from_html(document: str, username: str) -> dict[str, A
235
257
  'profile_pic_url': avatar_url,
236
258
  '_html_posts': posts,
237
259
  }
238
- return user if any((description, followers, following, post_count, country, posts)) else {}
260
+ return user if any((biography, followers, following, post_count, country, posts)) else {}
239
261
 
240
262
 
241
263
  def _fetch_web_profile_user(username: str) -> dict[str, Any] | None:
@@ -261,6 +283,15 @@ def _fetch_web_profile_user(username: str) -> dict[str, Any] | None:
261
283
  merged[key] = value
262
284
  return merged
263
285
 
286
+ def profile_is_complete(user: Mapping[str, Any]) -> bool:
287
+ if profile_quality(user) < 4:
288
+ return False
289
+ if platform.system() != 'Windows':
290
+ return True
291
+ has_bio = bool(str(user.get('biography') or user.get('bio') or '').strip())
292
+ has_following = any(user.get(key) not in (None, '', {}, []) for key in ('edge_follow', 'follows', 'following', 'following_count', 'followingCount'))
293
+ return has_bio and has_following
294
+
264
295
  best_user: dict[str, Any] | None = None
265
296
  for endpoint in endpoints:
266
297
  try:
@@ -273,7 +304,7 @@ def _fetch_web_profile_user(username: str) -> dict[str, Any] | None:
273
304
  user = ((payload.get('data') or {}).get('user') if isinstance(payload, dict) else None)
274
305
  if isinstance(user, dict):
275
306
  best_user = merge_profile(best_user, user)
276
- if profile_quality(best_user) >= 4:
307
+ if profile_is_complete(best_user):
277
308
  return best_user
278
309
 
279
310
  curl = shutil.which('curl')
@@ -453,6 +484,8 @@ def fetch_public_instagram_profile(source: str) -> IntegrationResult:
453
484
  api_user = _fetch_web_profile_user(reference['username'])
454
485
  if api_user:
455
486
  return IntegrationResult(True, 'Perfil Instagram encontrado publicamente.', _profile_data_from_api(api_user, reference))
487
+ if platform.system() == 'Windows':
488
+ return IntegrationResult(False, 'Não foi possível encontrar o perfil público estruturado do Instagram.', reference)
456
489
  try:
457
490
  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)
458
491
  except requests.RequestException as exc:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.6.59",
3
+ "version": "0.6.61",
4
4
  "description": "Thunderbolt — interface local para operação de canais faceless e motor MoneyPrinterTurbo",
5
5
  "license": "MIT",
6
6
  "main": "scripts/cli.mjs",