@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
|
|
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
|
|
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
|
-
|
|
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