@danhachuel/thunderbolt 0.6.19 → 0.6.21

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.
@@ -29,6 +29,14 @@ def _metric(value: Any) -> str:
29
29
  return _clean(value) or "—"
30
30
 
31
31
 
32
+ def _profile_metric(profile: Mapping[str, Any], *keys: str) -> Any:
33
+ for key in keys:
34
+ value = profile.get(key)
35
+ if value not in (None, ""):
36
+ return value
37
+ return None
38
+
39
+
32
40
  def _language_index(value: Any) -> int:
33
41
  code = language_code(value)
34
42
  return list(LANGUAGE_CODES).index(code) if code in LANGUAGE_CODES else list(LANGUAGE_CODES).index("pt")
@@ -297,16 +305,20 @@ def _render_instagram_card(profile: dict[str, Any], characters: list[dict[str, A
297
305
  with header_cols[2]:
298
306
  st.metric("posts", _metric(profile.get("post_count")))
299
307
  with header_cols[3]:
300
- st.metric("Seguidores", _metric(profile.get("subscriber_count")))
308
+ st.metric("Seguidores", _metric(_profile_metric(profile, "subscriber_count", "followers_count", "follower_count")))
301
309
  with header_cols[4]:
302
- st.metric("seguindo", _metric(profile.get("following_count")))
310
+ st.metric("seguindo", _metric(_profile_metric(profile, "following_count", "following", "follows")))
303
311
  with header_cols[5]:
304
312
  refresh_col, edit_col = st.columns(2)
305
313
  with refresh_col:
306
314
  if st.button("↻", help="Actualizar posts, seguidores e seguindo", key=f"refresh_instagram_{profile_id}"):
307
315
  result = fetch_public_instagram_profile(_clean(profile.get("url")) or _clean(profile.get("handle")))
308
316
  if result.ok:
309
- update_channel(profile_id, {**result.data, "platform": "instagram", "country": profile.get("country", ""), "language": profile.get("language", ""), "character_id": profile.get("character_id", "")})
317
+ refreshed = dict(result.data)
318
+ for field in ("subscriber_count", "following_count", "post_count", "bio", "avatar_url"):
319
+ if refreshed.get(field) in (None, "") and profile.get(field) not in (None, ""):
320
+ refreshed[field] = profile[field]
321
+ update_channel(profile_id, {**refreshed, "platform": "instagram", "country": profile.get("country", ""), "language": profile.get("language", ""), "character_id": profile.get("character_id", "")})
310
322
  st.success("Métricas Instagram actualizadas.")
311
323
  st.rerun()
312
324
  st.warning(result.message)
@@ -5,7 +5,7 @@ import json
5
5
  from dataclasses import dataclass
6
6
  from datetime import datetime, timezone
7
7
  from html import unescape
8
- from typing import Any
8
+ from typing import Any, Mapping
9
9
  from urllib.parse import urlparse
10
10
 
11
11
  import requests
@@ -67,6 +67,65 @@ def _structured_metric(document: str, *keys: str) -> int | None:
67
67
  return None
68
68
 
69
69
 
70
+ def _structured_metric_from_json(document: str, *keys: str) -> int | None:
71
+ wanted = {key.casefold() for key in keys}
72
+ for parsed in _embedded_json_documents(document):
73
+ for node in _walk_json(parsed):
74
+ if not isinstance(node, dict):
75
+ continue
76
+ for key, value in node.items():
77
+ if str(key).casefold() not in wanted:
78
+ continue
79
+ if isinstance(value, dict):
80
+ value = value.get('count') or value.get('value')
81
+ try:
82
+ return int(value)
83
+ except (TypeError, ValueError):
84
+ continue
85
+ return None
86
+
87
+
88
+ def _fetch_web_profile_user(username: str) -> dict[str, Any] | None:
89
+ try:
90
+ response = requests.get(
91
+ f'https://i.instagram.com/api/v1/users/web_profile_info/?username={username}',
92
+ headers={
93
+ 'x-ig-app-id': '936619743392459',
94
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/131.0 Safari/537.36',
95
+ 'Accept': '*/*',
96
+ 'Accept-Language': 'en-US,en;q=0.9',
97
+ },
98
+ timeout=15,
99
+ )
100
+ if response.status_code >= 400:
101
+ return None
102
+ payload = response.json()
103
+ except (requests.RequestException, ValueError, AttributeError):
104
+ return None
105
+ user = ((payload.get('data') or {}).get('user') if isinstance(payload, dict) else None)
106
+ return user if isinstance(user, dict) else None
107
+
108
+
109
+ def _profile_data_from_api(user: Mapping[str, Any], reference: Mapping[str, str]) -> dict[str, Any]:
110
+ followers = _structured_metric_from_json(json.dumps(user), 'edge_followed_by', 'followers', 'follower_count')
111
+ following = _structured_metric_from_json(json.dumps(user), 'edge_follow', 'follows', 'following', 'following_count')
112
+ media = ((user.get('edge_owner_to_timeline_media') or {}).get('count') if isinstance(user.get('edge_owner_to_timeline_media'), dict) else None)
113
+ return {
114
+ 'id': f"instagram_{reference['username']}",
115
+ **reference,
116
+ 'name': str(user.get('full_name') or user.get('username') or reference['username']).strip(),
117
+ 'bio': str(user.get('biography') or '').strip(),
118
+ 'avatar_url': str(user.get('profile_pic_url_hd') or user.get('profile_pic_url') or '').strip(),
119
+ 'subscriber_count': followers,
120
+ 'following_count': following,
121
+ 'post_count': media,
122
+ 'public_lookup': True,
123
+ 'metrics_source': 'instagram_web_profile_info',
124
+ 'last_public_lookup_at': datetime.now(timezone.utc).isoformat(),
125
+ '_api_posts': (((user.get('edge_owner_to_timeline_media') or {}).get('edges') or []) if isinstance(user.get('edge_owner_to_timeline_media'), dict) else []),
126
+ }
127
+
128
+
70
129
  def fetch_public_instagram_profile(source: str) -> IntegrationResult:
71
130
  try:
72
131
  reference = normalize_instagram_reference(source)
@@ -92,9 +151,13 @@ def fetch_public_instagram_profile(source: str) -> IntegrationResult:
92
151
  continue
93
152
  return None
94
153
 
95
- followers = metric((r'([\d,.]+)\s*(?:mil\s+)?(?:followers|seguidores)',)) or _structured_metric(response.text, 'edge_followed_by', 'followers', 'follower_count')
96
- following = metric((r'([\d,.]+)\s*(?:mil\s+)?(?:following|seguindo)',)) or _structured_metric(response.text, 'edge_follow', 'follows', 'following', 'following_count', 'followingCount')
154
+ followers = metric((r'([\d,.]+)\s*(?:mil\s+)?(?:followers|seguidores)',)) or _structured_metric(response.text, 'edge_followed_by', 'followers', 'follower_count') or _structured_metric_from_json(response.text, 'edge_followed_by', 'followers', 'follower_count', 'followerCount')
155
+ following = metric((r'([\d,.]+)\s*(?:mil\s+)?(?:following|seguindo)',)) or _structured_metric(response.text, 'edge_follow', 'follows', 'following', 'following_count', 'followingCount') or _structured_metric_from_json(response.text, 'edge_follow', 'follows', 'following', 'following_count', 'followingCount')
97
156
  posts = metric((r'([\d,.]+)\s*(?:mil\s+)?(?:posts|publicações|publications)',)) or _structured_metric(response.text, 'edge_owner_to_timeline_media', 'posts', 'post_count')
157
+ if followers is None or following is None or not avatar_url:
158
+ api_user = _fetch_web_profile_user(reference['username'])
159
+ if api_user:
160
+ return IntegrationResult(True, 'Perfil Instagram encontrado publicamente.', _profile_data_from_api(api_user, reference))
98
161
  data = {'id': f"instagram_{reference['username']}", **reference, 'name': title.split('(')[0].strip() or reference['username'], 'bio': description, 'avatar_url': avatar_url, 'subscriber_count': followers, 'following_count': following, 'post_count': posts, 'public_lookup': True, 'metrics_source': 'instagram_public_page', 'last_public_lookup_at': datetime.now(timezone.utc).isoformat()}
99
162
  return IntegrationResult(True, 'Perfil Instagram encontrado publicamente.', data)
100
163
 
@@ -161,6 +224,17 @@ def fetch_public_instagram_posts(source: str, limit: int = 10) -> IntegrationRes
161
224
  reference = normalize_instagram_reference(source)
162
225
  except ValueError as exc:
163
226
  return IntegrationResult(False, str(exc), {})
227
+ api_user = _fetch_web_profile_user(reference['username'])
228
+ if api_user:
229
+ api_posts: list[dict[str, Any]] = []
230
+ for edge in ((api_user.get('edge_owner_to_timeline_media') or {}).get('edges') or []):
231
+ node = edge.get('node') if isinstance(edge, dict) else None
232
+ if isinstance(node, dict):
233
+ post = _post_from_node(node)
234
+ if post:
235
+ api_posts.append(post)
236
+ if api_posts:
237
+ return IntegrationResult(True, 'Posts públicos encontrados.', reference | {'posts': api_posts[:max(1, int(limit))]})
164
238
  try:
165
239
  response = requests.get(reference['url'], headers={'User-Agent': 'Mozilla/5.0', 'Accept-Language': 'pt-BR,pt;q=0.9,en;q=0.8'}, timeout=15)
166
240
  except requests.RequestException as exc:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.6.19",
3
+ "version": "0.6.21",
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",