@danhachuel/thunderbolt 0.6.54 → 0.6.56
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/app/social_networks_ui.py +72 -15
- package/integrations/instagram_public.py +106 -40
- package/package.json +1 -1
|
@@ -17,7 +17,13 @@ from hermes_ui.influencers import InfluencerBackendError, STANDALONE_CONTENT_INF
|
|
|
17
17
|
from hermes_ui.storage import read_json, write_json
|
|
18
18
|
from hermes_ui.countries import COUNTRY_OPTIONS
|
|
19
19
|
from hermes_ui.languages import LANGUAGE_CODES, language_code, language_label
|
|
20
|
-
from integrations.instagram_public import
|
|
20
|
+
from integrations.instagram_public import (
|
|
21
|
+
extract_public_instagram_country,
|
|
22
|
+
fetch_public_instagram_posts,
|
|
23
|
+
fetch_public_instagram_profile,
|
|
24
|
+
normalize_instagram_bio,
|
|
25
|
+
normalize_instagram_metric,
|
|
26
|
+
)
|
|
21
27
|
from integrations.meta_social import test_facebook_pages_api_card, test_instagram_api_card
|
|
22
28
|
|
|
23
29
|
|
|
@@ -72,11 +78,35 @@ def _country_key(value: Any) -> str:
|
|
|
72
78
|
COUNTRY_ALIASES = {
|
|
73
79
|
"brazil": "Brasil",
|
|
74
80
|
"brasil": "Brasil",
|
|
81
|
+
"br": "Brasil",
|
|
75
82
|
"usa": "Estados Unidos",
|
|
76
83
|
"us": "Estados Unidos",
|
|
77
84
|
"united states": "Estados Unidos",
|
|
78
85
|
"united states of america": "Estados Unidos",
|
|
86
|
+
"pt": "Portugal",
|
|
79
87
|
"portugal": "Portugal",
|
|
88
|
+
"uk": "Reino Unido",
|
|
89
|
+
"gb": "Reino Unido",
|
|
90
|
+
"united kingdom": "Reino Unido",
|
|
91
|
+
"england": "Reino Unido",
|
|
92
|
+
"germany": "Alemanha",
|
|
93
|
+
"de": "Alemanha",
|
|
94
|
+
"france": "França",
|
|
95
|
+
"fr": "França",
|
|
96
|
+
"italy": "Itália",
|
|
97
|
+
"it": "Itália",
|
|
98
|
+
"spain": "Espanha",
|
|
99
|
+
"es": "Espanha",
|
|
100
|
+
"canada": "Canadá",
|
|
101
|
+
"ca": "Canadá",
|
|
102
|
+
"australia": "Austrália",
|
|
103
|
+
"au": "Austrália",
|
|
104
|
+
"japan": "Japão",
|
|
105
|
+
"jp": "Japão",
|
|
106
|
+
"mexico": "México",
|
|
107
|
+
"mx": "México",
|
|
108
|
+
"argentina": "Argentina",
|
|
109
|
+
"ar": "Argentina",
|
|
80
110
|
}
|
|
81
111
|
|
|
82
112
|
|
|
@@ -94,11 +124,17 @@ def _normalise_country(value: Any) -> str:
|
|
|
94
124
|
|
|
95
125
|
|
|
96
126
|
def _profile_country(profile: Mapping[str, Any]) -> str:
|
|
97
|
-
for key in ("country", "country_name", "country_of_registration", "account_country", "account_based_in"):
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
127
|
+
for key in ("country", "country_name", "country_of_registration", "account_country", "account_based_in", "country_code"):
|
|
128
|
+
raw_value = profile.get(key)
|
|
129
|
+
candidates = [raw_value]
|
|
130
|
+
if isinstance(raw_value, Mapping):
|
|
131
|
+
candidates.extend(raw_value.get(child) for child in ("name", "label", "display_name", "country", "country_name", "code"))
|
|
132
|
+
for candidate in candidates:
|
|
133
|
+
country = _normalise_country(candidate)
|
|
134
|
+
if country:
|
|
135
|
+
return country
|
|
136
|
+
extracted = _normalise_country(extract_public_instagram_country(profile))
|
|
137
|
+
return extracted
|
|
102
138
|
|
|
103
139
|
|
|
104
140
|
def _language_index(value: Any) -> int:
|
|
@@ -130,9 +166,13 @@ def _instagram_profile_storage_id(profile: Mapping[str, Any]) -> str:
|
|
|
130
166
|
|
|
131
167
|
|
|
132
168
|
def _clear_instagram_result_widget_state(profile: Mapping[str, Any]) -> None:
|
|
169
|
+
"""Clear every previous widget key for this profile before rebuilding the form."""
|
|
133
170
|
widget_id = _instagram_profile_storage_id(profile)
|
|
134
171
|
for field in ("name", "bio", "country", "language", "posts", "followers", "following", "character"):
|
|
135
|
-
|
|
172
|
+
prefix = f"social_instagram_result_{field}_{widget_id}"
|
|
173
|
+
for key in list(st.session_state.keys()):
|
|
174
|
+
if str(key).startswith(prefix):
|
|
175
|
+
st.session_state.pop(key, None)
|
|
136
176
|
|
|
137
177
|
|
|
138
178
|
def _instagram_result_widget_id(profile: Mapping[str, Any]) -> str:
|
|
@@ -143,9 +183,23 @@ def _instagram_result_widget_id(profile: Mapping[str, Any]) -> str:
|
|
|
143
183
|
|
|
144
184
|
|
|
145
185
|
def _normalise_instagram_posts(value: Any) -> list[dict[str, Any]]:
|
|
186
|
+
if isinstance(value, Mapping):
|
|
187
|
+
value = value.get("posts") or value.get("items") or value.get("edges") or []
|
|
146
188
|
if not isinstance(value, list):
|
|
147
189
|
return []
|
|
148
|
-
|
|
190
|
+
normalised: list[dict[str, Any]] = []
|
|
191
|
+
seen: set[str] = set()
|
|
192
|
+
for item in value:
|
|
193
|
+
post = item.get("node") if isinstance(item, Mapping) and isinstance(item.get("node"), Mapping) else item
|
|
194
|
+
if not isinstance(post, Mapping):
|
|
195
|
+
continue
|
|
196
|
+
data = dict(post)
|
|
197
|
+
identifier = _clean(data.get("id") or data.get("shortcode") or data.get("url"))
|
|
198
|
+
if not identifier or identifier in seen:
|
|
199
|
+
continue
|
|
200
|
+
seen.add(identifier)
|
|
201
|
+
normalised.append(data)
|
|
202
|
+
return normalised
|
|
149
203
|
|
|
150
204
|
|
|
151
205
|
def _stored_instagram_posts(profile: Mapping[str, Any]) -> list[dict[str, Any]]:
|
|
@@ -219,12 +273,11 @@ def _render_instagram_posts(profile: Mapping[str, Any]) -> None:
|
|
|
219
273
|
# DEBUG WINDOWS INSTAGRAM: manter até a validação final do carregamento público.
|
|
220
274
|
st.write({"instagram_posts_ok": posts_ok, "instagram_posts_message": posts_message, "instagram_posts_count": len(loaded_posts)})
|
|
221
275
|
if posts_ok:
|
|
222
|
-
posts = loaded_posts
|
|
276
|
+
posts = _normalise_instagram_posts(loaded_posts)
|
|
223
277
|
_persist_instagram_posts(profile, posts)
|
|
224
278
|
st.success(f"{len(posts)} posts carregados.")
|
|
225
|
-
st.rerun()
|
|
226
279
|
else:
|
|
227
|
-
st.
|
|
280
|
+
st.error(posts_message)
|
|
228
281
|
posts = st.session_state.get(state_key, posts)
|
|
229
282
|
if not posts:
|
|
230
283
|
st.info("Clique em Carregar últimos 10 para consultar os posts públicos desta conta.")
|
|
@@ -243,9 +296,10 @@ def _render_instagram_posts(profile: Mapping[str, Any]) -> None:
|
|
|
243
296
|
if st.button("Mostrar + 10", key=f"more_instagram_posts_{profile_id}", use_container_width=True):
|
|
244
297
|
posts_ok, posts_message, loaded_posts = _load_instagram_posts(profile, limit=len(posts) + 10)
|
|
245
298
|
if posts_ok:
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
299
|
+
posts = _normalise_instagram_posts(loaded_posts)
|
|
300
|
+
_persist_instagram_posts(profile, posts)
|
|
301
|
+
else:
|
|
302
|
+
st.error(posts_message)
|
|
249
303
|
|
|
250
304
|
|
|
251
305
|
def _api_card_defaults(kind: str) -> tuple[str, list[str], Callable[[Mapping[str, Any]], dict[str, Any]]]:
|
|
@@ -630,7 +684,10 @@ def render_social_networks(settings: dict[str, Any]) -> None:
|
|
|
630
684
|
st.session_state["social_instagram_ok"] = result.ok
|
|
631
685
|
st.session_state["social_instagram_message"] = result.message
|
|
632
686
|
if st.session_state.get("social_instagram_message"):
|
|
633
|
-
|
|
687
|
+
if st.session_state.get("social_instagram_ok"):
|
|
688
|
+
st.success(st.session_state["social_instagram_message"])
|
|
689
|
+
else:
|
|
690
|
+
st.error(st.session_state["social_instagram_message"])
|
|
634
691
|
data = st.session_state.get("social_instagram_result", {}) if st.session_state.get("social_instagram_ok") else {}
|
|
635
692
|
if data:
|
|
636
693
|
characters, _ = _characters(settings)
|
|
@@ -191,6 +191,14 @@ def _fetch_profile_instaloader(username: str) -> dict[str, Any] | None:
|
|
|
191
191
|
)
|
|
192
192
|
profile = instaloader.Profile.from_username(loader.context, username)
|
|
193
193
|
LOGGER.info('Fallback instaloader activado para o perfil @%s', username)
|
|
194
|
+
country = next(
|
|
195
|
+
(
|
|
196
|
+
str(getattr(profile, attribute, '') or '').strip()
|
|
197
|
+
for attribute in ('country', 'country_name', 'country_of_registration', 'account_country', 'account_based_in')
|
|
198
|
+
if str(getattr(profile, attribute, '') or '').strip()
|
|
199
|
+
),
|
|
200
|
+
'',
|
|
201
|
+
)
|
|
194
202
|
return {
|
|
195
203
|
'id': str(getattr(profile, 'userid', '') or ''),
|
|
196
204
|
'username': str(getattr(profile, 'username', '') or username),
|
|
@@ -199,6 +207,7 @@ def _fetch_profile_instaloader(username: str) -> dict[str, Any] | None:
|
|
|
199
207
|
'follower_count': getattr(profile, 'follower_count', getattr(profile, 'followers', None)),
|
|
200
208
|
'following_count': getattr(profile, 'following_count', getattr(profile, 'followees', None)),
|
|
201
209
|
'edge_owner_to_timeline_media': {'count': getattr(profile, 'mediacount', None)},
|
|
210
|
+
'country': country,
|
|
202
211
|
'is_private': bool(getattr(profile, 'is_private', False)),
|
|
203
212
|
'_instagram_source': 'instaloader',
|
|
204
213
|
}
|
|
@@ -230,6 +239,10 @@ def _fetch_posts_instaloader(username: str, limit: int = 10) -> list[dict[str, A
|
|
|
230
239
|
continue
|
|
231
240
|
image_url = str(getattr(post, 'url', '') or '')
|
|
232
241
|
timestamp = getattr(post, 'date_utc', '') or ''
|
|
242
|
+
if hasattr(timestamp, 'isoformat'):
|
|
243
|
+
timestamp = timestamp.isoformat()
|
|
244
|
+
else:
|
|
245
|
+
timestamp = str(timestamp)
|
|
233
246
|
posts.append({
|
|
234
247
|
'id': str(getattr(post, 'mediaid', '') or shortcode),
|
|
235
248
|
'shortcode': shortcode,
|
|
@@ -367,8 +380,17 @@ def _extract_profile_user(payload: Any) -> Mapping[str, Any] | None:
|
|
|
367
380
|
(payload.get('data') or {}).get('user') if isinstance(payload.get('data'), Mapping) else None,
|
|
368
381
|
(payload.get('graphql') or {}).get('user') if isinstance(payload.get('graphql'), Mapping) else None,
|
|
369
382
|
(payload.get('data') or {}).get('profile') if isinstance(payload.get('data'), Mapping) else None,
|
|
383
|
+
(payload.get('xdt_api__v1__users__web_profile_info') or {}).get('user') if isinstance(payload.get('xdt_api__v1__users__web_profile_info'), Mapping) else None,
|
|
370
384
|
]
|
|
371
|
-
|
|
385
|
+
direct = next((candidate for candidate in candidates if isinstance(candidate, Mapping)), None)
|
|
386
|
+
if direct is not None:
|
|
387
|
+
return direct
|
|
388
|
+
for node in _walk_json(payload):
|
|
389
|
+
if not isinstance(node, Mapping) or not node.get('username'):
|
|
390
|
+
continue
|
|
391
|
+
if any(key in node for key in ('biography', 'edge_follow', 'edge_followed_by', 'follower_count', 'following_count', 'full_name')):
|
|
392
|
+
return node
|
|
393
|
+
return None
|
|
372
394
|
|
|
373
395
|
|
|
374
396
|
def _shared_data_documents(document: str) -> list[Any]:
|
|
@@ -417,7 +439,7 @@ def _profile_users_from_document(document: str) -> list[Mapping[str, Any]]:
|
|
|
417
439
|
|
|
418
440
|
|
|
419
441
|
def _fetch_profile_with_playwright(username: str, headers: Mapping[str, str] | None = None, cookies: Mapping[str, str] | None = None) -> dict[str, Any] | None:
|
|
420
|
-
"""Fetch
|
|
442
|
+
"""Fetch a public profile using browser state, API responses and embedded HTML data."""
|
|
421
443
|
headers = headers or _instagram_headers()
|
|
422
444
|
cookies = cookies or _instagram_cookies()
|
|
423
445
|
captured_user: dict[str, Any] | None = None
|
|
@@ -438,16 +460,20 @@ def _fetch_profile_with_playwright(username: str, headers: Mapping[str, str] | N
|
|
|
438
460
|
|
|
439
461
|
def capture_profile_response(response: Any) -> None:
|
|
440
462
|
nonlocal captured_user
|
|
441
|
-
|
|
463
|
+
response_url = str(getattr(response, 'url', '') or '')
|
|
464
|
+
if not any(marker in response_url for marker in ('/api/v1/users/web_profile_info/', '/graphql/query')):
|
|
442
465
|
return
|
|
466
|
+
status = getattr(response, 'status', None)
|
|
467
|
+
LOGGER.info('Instagram Playwright API response URL=%s status=%s', response_url, status)
|
|
443
468
|
try:
|
|
444
469
|
payload = response.json()
|
|
445
|
-
user = (
|
|
470
|
+
user = _extract_profile_user(payload)
|
|
446
471
|
if isinstance(user, Mapping):
|
|
447
472
|
captured_user = _merge_profile(captured_user, user)
|
|
448
|
-
LOGGER.
|
|
473
|
+
LOGGER.info('Instagram Playwright perfil capturado URL=%s status=%s quality=%s', response_url, status, _profile_quality(captured_user))
|
|
449
474
|
except (ValueError, TypeError, AttributeError) as exc:
|
|
450
|
-
LOGGER.
|
|
475
|
+
LOGGER.warning('Não foi possível interpretar resposta Instagram URL=%s status=%s: %s', response_url, status, exc)
|
|
476
|
+
|
|
451
477
|
|
|
452
478
|
page.on('response', capture_profile_response)
|
|
453
479
|
try:
|
|
@@ -458,21 +484,36 @@ def _fetch_profile_with_playwright(username: str, headers: Mapping[str, str] | N
|
|
|
458
484
|
except Exception as exc:
|
|
459
485
|
LOGGER.debug('Instagram Playwright não carregou a página inicial: %s', exc)
|
|
460
486
|
url = f'https://www.instagram.com/{username}/'
|
|
461
|
-
response =
|
|
462
|
-
|
|
463
|
-
if response is None or response.status >= 400:
|
|
464
|
-
LOGGER.warning('Instagram Playwright perfil HTTP status=%s URL=%s', response.status if response else None, url)
|
|
465
|
-
return captured_user
|
|
487
|
+
response = None
|
|
488
|
+
status = None
|
|
466
489
|
try:
|
|
467
|
-
|
|
490
|
+
response = page.goto(url, wait_until='domcontentloaded', timeout=30000)
|
|
491
|
+
status = response.status if response else None
|
|
468
492
|
except Exception as exc:
|
|
469
|
-
LOGGER.
|
|
470
|
-
|
|
493
|
+
LOGGER.warning('Instagram Playwright navegação falhou URL=%s: %s', url, exc)
|
|
494
|
+
LOGGER.info('Instagram Playwright profile URL=%s status=%s', url, status)
|
|
495
|
+
try:
|
|
496
|
+
page.wait_for_timeout(5000)
|
|
497
|
+
except Exception:
|
|
498
|
+
pass
|
|
471
499
|
try:
|
|
472
500
|
page.wait_for_load_state('networkidle', timeout=10000)
|
|
473
501
|
except (PlaywrightTimeoutError, TimeoutError):
|
|
474
|
-
LOGGER.debug('Instagram Playwright networkidle excedido; usando
|
|
475
|
-
|
|
502
|
+
LOGGER.debug('Instagram Playwright networkidle excedido; usando dados já capturados.')
|
|
503
|
+
try:
|
|
504
|
+
document = page.content()
|
|
505
|
+
_save_instagram_debug_html(username, document)
|
|
506
|
+
for user in _profile_users_from_document(document):
|
|
507
|
+
if isinstance(user, Mapping):
|
|
508
|
+
candidate = _merge_profile(captured_user, user)
|
|
509
|
+
if _profile_quality(candidate) >= _profile_quality(captured_user or {}):
|
|
510
|
+
captured_user = candidate
|
|
511
|
+
except Exception as exc:
|
|
512
|
+
LOGGER.warning('Não foi possível guardar/analisar HTML de diagnóstico URL=%s: %s', url, exc)
|
|
513
|
+
if response is None or status is None or status >= 400:
|
|
514
|
+
LOGGER.warning('Instagram Playwright perfil HTTP status=%s URL=%s', status, url)
|
|
515
|
+
return captured_user
|
|
516
|
+
LOGGER.info('Instagram Playwright perfil final captured=%s quality=%s URL=%s', bool(captured_user), _profile_quality(captured_user or {}), url)
|
|
476
517
|
finally:
|
|
477
518
|
browser.close()
|
|
478
519
|
except Exception as exc:
|
|
@@ -631,6 +672,30 @@ def _country_from_bio_fallback(bio: str) -> str:
|
|
|
631
672
|
return ''
|
|
632
673
|
|
|
633
674
|
|
|
675
|
+
def _profile_post_nodes(user: Mapping[str, Any]) -> list[Mapping[str, Any]]:
|
|
676
|
+
"""Return post nodes from both legacy GraphQL edges and current items payloads."""
|
|
677
|
+
containers: list[Any] = [
|
|
678
|
+
user.get('edge_owner_to_timeline_media'),
|
|
679
|
+
user.get('timeline_media'),
|
|
680
|
+
user.get('media'),
|
|
681
|
+
user,
|
|
682
|
+
]
|
|
683
|
+
nodes: list[Mapping[str, Any]] = []
|
|
684
|
+
for container in containers:
|
|
685
|
+
if not isinstance(container, Mapping):
|
|
686
|
+
continue
|
|
687
|
+
raw_items = container.get('edges') or container.get('items') or container.get('data') or []
|
|
688
|
+
if isinstance(raw_items, Mapping):
|
|
689
|
+
raw_items = raw_items.get('items') or raw_items.get('edges') or []
|
|
690
|
+
if not isinstance(raw_items, list):
|
|
691
|
+
continue
|
|
692
|
+
for item in raw_items:
|
|
693
|
+
node = item.get('node') if isinstance(item, Mapping) and isinstance(item.get('node'), Mapping) else item
|
|
694
|
+
if isinstance(node, Mapping):
|
|
695
|
+
nodes.append(node)
|
|
696
|
+
return nodes
|
|
697
|
+
|
|
698
|
+
|
|
634
699
|
def _profile_data_from_api(user: Mapping[str, Any], reference: Mapping[str, str]) -> dict[str, Any]:
|
|
635
700
|
followers = _profile_metric(user, 'edge_followed_by', 'followers', 'follower_count', 'followerCount')
|
|
636
701
|
following_node = user.get('edge_follow')
|
|
@@ -638,10 +703,12 @@ def _profile_data_from_api(user: Mapping[str, Any], reference: Mapping[str, str]
|
|
|
638
703
|
following = following if following is not None else _profile_metric(user, 'follows', 'following', 'following_count', 'followingCount')
|
|
639
704
|
followers = followers if followers is not None else _structured_metric_from_json(json.dumps(user), 'edge_followed_by', 'followers', 'follower_count', 'followerCount')
|
|
640
705
|
following = following if following is not None else _structured_metric_from_json(json.dumps(user), 'edge_follow', 'follows', 'following', 'following_count', 'followingCount')
|
|
641
|
-
|
|
706
|
+
media_node = user.get('edge_owner_to_timeline_media') or user.get('media')
|
|
707
|
+
media = (media_node.get('count') if isinstance(media_node, Mapping) else None)
|
|
642
708
|
media = media if media is not None else _profile_metric(user, 'posts', 'post_count', 'media_count')
|
|
643
709
|
biography = str(user.get('biography') or user.get('bio') or user.get('description') or '').strip()
|
|
644
|
-
|
|
710
|
+
direct_country = _country_value(user.get('country') or user.get('country_code'))
|
|
711
|
+
country = direct_country or extract_public_instagram_country(user) or _fetch_instagram_about_country(user.get('id') or user.get('pk'))
|
|
645
712
|
country = country or _country_from_bio_fallback(biography)
|
|
646
713
|
LOGGER.debug('Instagram profile extracted keys=%s edge_follow=%r bio=%s followers=%s following=%s posts=%s country=%s', sorted(str(key) for key in user.keys()), following_node, bool(biography), followers, following, media, country or '—')
|
|
647
714
|
return {
|
|
@@ -659,7 +726,7 @@ def _profile_data_from_api(user: Mapping[str, Any], reference: Mapping[str, str]
|
|
|
659
726
|
'public_lookup': True,
|
|
660
727
|
'metrics_source': 'instagram_web_profile_info',
|
|
661
728
|
'last_public_lookup_at': datetime.now(timezone.utc).isoformat(),
|
|
662
|
-
'_api_posts':
|
|
729
|
+
'_api_posts': [dict(node) for node in _profile_post_nodes(user)],
|
|
663
730
|
}
|
|
664
731
|
|
|
665
732
|
|
|
@@ -674,10 +741,14 @@ def normalize_instagram_metric(value: Any) -> int | None:
|
|
|
674
741
|
|
|
675
742
|
|
|
676
743
|
PUBLIC_COUNTRY_KEYS = {
|
|
677
|
-
'about_this_account_country', 'account_based_in', 'account_country',
|
|
678
|
-
'
|
|
744
|
+
'about_this_account_country', 'account_based_in', 'account_country',
|
|
745
|
+
'country_name', 'country_of_origin', 'country_of_registration', 'location_country',
|
|
746
|
+
'transparency_country',
|
|
747
|
+
}
|
|
748
|
+
PUBLIC_TRANSPARENCY_KEYS = {
|
|
749
|
+
'about', 'about_account', 'about_this_account', 'about_this_account_data',
|
|
750
|
+
'account_transparency', 'transparency', 'account_info', 'profile_context',
|
|
679
751
|
}
|
|
680
|
-
PUBLIC_TRANSPARENCY_KEYS = {'about', 'about_account', 'account_transparency', 'transparency', 'account_info'}
|
|
681
752
|
|
|
682
753
|
|
|
683
754
|
def _country_value(value: Any) -> str:
|
|
@@ -733,7 +804,7 @@ def fetch_public_instagram_profile(source: str) -> IntegrationResult:
|
|
|
733
804
|
return IntegrationResult(True, 'Perfil encontrado via fallback instaloader. O país pode não estar disponível.', data)
|
|
734
805
|
return IntegrationResult(True, 'Perfil Instagram encontrado publicamente.', data)
|
|
735
806
|
if platform.system() == 'Windows':
|
|
736
|
-
return IntegrationResult(False, 'Não foi possível
|
|
807
|
+
return IntegrationResult(False, 'Não foi possível obter os dados do Instagram. Verifique sua conexão ou tente novamente mais tarde.', reference)
|
|
737
808
|
try:
|
|
738
809
|
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)
|
|
739
810
|
except requests.RequestException as exc:
|
|
@@ -848,18 +919,15 @@ def _fetch_posts_with_playwright(reference: Mapping[str, str], limit: int, cooki
|
|
|
848
919
|
page = context.new_page()
|
|
849
920
|
|
|
850
921
|
def capture_profile_posts(response: Any) -> None:
|
|
851
|
-
|
|
922
|
+
response_url = str(getattr(response, 'url', '') or '')
|
|
923
|
+
if not any(marker in response_url for marker in ('/api/v1/users/web_profile_info/', '/graphql/query')):
|
|
852
924
|
return
|
|
925
|
+
LOGGER.info('Instagram Playwright posts response URL=%s status=%s', response_url, getattr(response, 'status', None))
|
|
853
926
|
try:
|
|
854
927
|
payload = response.json()
|
|
855
|
-
user = (
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
for edge in edges or []:
|
|
859
|
-
node = edge.get('node') if isinstance(edge, Mapping) else None
|
|
860
|
-
if not isinstance(node, dict):
|
|
861
|
-
continue
|
|
862
|
-
post = _post_from_node(node)
|
|
928
|
+
user = _extract_profile_user(payload)
|
|
929
|
+
for node in _profile_post_nodes(user) if isinstance(user, Mapping) else []:
|
|
930
|
+
post = _post_from_node(dict(node))
|
|
863
931
|
if post and post['id'] not in seen:
|
|
864
932
|
seen.add(post['id'])
|
|
865
933
|
posts.append(post)
|
|
@@ -909,16 +977,14 @@ def fetch_public_instagram_posts(source: str, limit: int = 10) -> IntegrationRes
|
|
|
909
977
|
_streamlit_message('info', 'Dados obtidos via fallback (instaloader). O país pode não estar disponível.', 'posts_instaloader_fallback')
|
|
910
978
|
else:
|
|
911
979
|
_streamlit_message('error', 'Não foi possível obter os dados do Instagram. Verifique sua conexão ou tente novamente mais tarde.', 'posts_instagram_failed')
|
|
912
|
-
return IntegrationResult(bool(posts), 'Posts públicos encontrados.' if posts else 'Não foi possível
|
|
980
|
+
return IntegrationResult(bool(posts), 'Posts públicos encontrados.' if posts else 'Não foi possível obter os dados do Instagram. Verifique sua conexão ou tente novamente mais tarde.', reference | {'posts': posts[:max(1, int(limit))]})
|
|
913
981
|
api_user = _fetch_web_profile_user(reference['username'])
|
|
914
982
|
if api_user:
|
|
915
983
|
api_posts: list[dict[str, Any]] = []
|
|
916
|
-
for
|
|
917
|
-
|
|
918
|
-
if
|
|
919
|
-
post
|
|
920
|
-
if post:
|
|
921
|
-
api_posts.append(post)
|
|
984
|
+
for node in _profile_post_nodes(api_user):
|
|
985
|
+
post = _post_from_node(dict(node))
|
|
986
|
+
if post:
|
|
987
|
+
api_posts.append(post)
|
|
922
988
|
if api_posts:
|
|
923
989
|
LOGGER.debug('Instagram API posts extracted=%s response=JSON', len(api_posts))
|
|
924
990
|
return IntegrationResult(True, 'Posts públicos encontrados.', reference | {'posts': api_posts[:max(1, int(limit))]})
|
|
@@ -948,7 +1014,7 @@ def fetch_public_instagram_posts(source: str, limit: int = 10) -> IntegrationRes
|
|
|
948
1014
|
if not posts:
|
|
949
1015
|
posts = _fetch_posts_with_playwright(reference, limit, _instagram_cookies())
|
|
950
1016
|
LOGGER.debug('Instagram posts extracted=%s limit=%s', len(posts), limit)
|
|
951
|
-
return IntegrationResult(bool(posts), 'Posts públicos encontrados.' if posts else 'Não foi possível
|
|
1017
|
+
return IntegrationResult(bool(posts), 'Posts públicos encontrados.' if posts else 'Não foi possível obter os dados do Instagram. Verifique sua conexão ou tente novamente mais tarde.', reference | {'posts': posts[:max(1, int(limit))]})
|
|
952
1018
|
|
|
953
1019
|
|
|
954
1020
|
__all__ = ['IntegrationResult', 'extract_public_instagram_country', 'fetch_posts_instaloader', 'fetch_profile_instaloader', 'fetch_public_instagram_posts', 'fetch_public_instagram_profile', 'normalize_instagram_bio', 'normalize_instagram_metric', 'normalize_instagram_reference']
|
package/package.json
CHANGED