@danhachuel/thunderbolt 0.6.56 → 0.6.57
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 +100 -274
- package/package.json +1 -1
- package/requirements.txt +0 -1
|
@@ -25,7 +25,6 @@ except ImportError: # pragma: no cover - Streamlit is a runtime dependency
|
|
|
25
25
|
|
|
26
26
|
LOGGER = logging.getLogger(__name__)
|
|
27
27
|
_WINDOWS_PLAYWRIGHT_CHECKED = False
|
|
28
|
-
_INSTALOADER_INSTALL_ATTEMPTED = False
|
|
29
28
|
|
|
30
29
|
|
|
31
30
|
ABOUT_ACCOUNT_URL = 'https://i.instagram.com/api/v1/bloks/apps/com.instagram.interactions.about_this_account/'
|
|
@@ -126,8 +125,8 @@ def _instagram_headers() -> dict[str, str]:
|
|
|
126
125
|
|
|
127
126
|
|
|
128
127
|
def _instagram_playwright_headless() -> bool:
|
|
129
|
-
"""Use
|
|
130
|
-
value = os.getenv('INSTAGRAM_PLAYWRIGHT_HEADLESS', '
|
|
128
|
+
"""Use headless mode by default; set INSTAGRAM_PLAYWRIGHT_HEADLESS=false for debug."""
|
|
129
|
+
value = os.getenv('INSTAGRAM_PLAYWRIGHT_HEADLESS', 'true').strip().casefold()
|
|
131
130
|
return value not in {'0', 'false', 'no', 'off'}
|
|
132
131
|
|
|
133
132
|
|
|
@@ -142,134 +141,6 @@ def _save_instagram_debug_html(username: str, document: str) -> None:
|
|
|
142
141
|
LOGGER.warning('Não foi possível salvar o HTML de diagnóstico Instagram: %s', exc)
|
|
143
142
|
|
|
144
143
|
|
|
145
|
-
def _instagram_instaloader():
|
|
146
|
-
"""Load the anonymous fallback, installing it once when the requirement is missing."""
|
|
147
|
-
global _INSTALOADER_INSTALL_ATTEMPTED
|
|
148
|
-
try:
|
|
149
|
-
import instaloader
|
|
150
|
-
return instaloader
|
|
151
|
-
except ImportError:
|
|
152
|
-
if _INSTALOADER_INSTALL_ATTEMPTED:
|
|
153
|
-
LOGGER.warning('Fallback instaloader indisponível; instale com: python -m pip install instaloader')
|
|
154
|
-
return None
|
|
155
|
-
_INSTALOADER_INSTALL_ATTEMPTED = True
|
|
156
|
-
try:
|
|
157
|
-
LOGGER.info('Instaloader ausente; a tentar instalar a dependência automaticamente.')
|
|
158
|
-
completed = subprocess.run(
|
|
159
|
-
[sys.executable, '-m', 'pip', 'install', 'instaloader>=4.14,<5'],
|
|
160
|
-
capture_output=True, text=True, encoding='utf-8', errors='replace', timeout=180, check=False,
|
|
161
|
-
)
|
|
162
|
-
if completed.returncode == 0:
|
|
163
|
-
import importlib
|
|
164
|
-
importlib.invalidate_caches()
|
|
165
|
-
try:
|
|
166
|
-
import instaloader
|
|
167
|
-
return instaloader
|
|
168
|
-
except ImportError:
|
|
169
|
-
LOGGER.warning('Instaloader foi instalado, mas não ficou disponível neste interpretador.')
|
|
170
|
-
LOGGER.warning('Instalação automática do instaloader falhou: %s', (completed.stderr or completed.stdout)[-1000:])
|
|
171
|
-
except (OSError, subprocess.SubprocessError) as exc:
|
|
172
|
-
LOGGER.warning('Não foi possível instalar instaloader automaticamente: %s', exc)
|
|
173
|
-
_streamlit_message('error', 'O fallback instaloader não está disponível. Instale com: python -m pip install instaloader', 'instaloader_missing')
|
|
174
|
-
return None
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
def _fetch_profile_instaloader(username: str) -> dict[str, Any] | None:
|
|
178
|
-
"""Fetch a public profile anonymously through Instaloader when Playwright fails."""
|
|
179
|
-
instaloader = _instagram_instaloader()
|
|
180
|
-
if instaloader is None:
|
|
181
|
-
return None
|
|
182
|
-
try:
|
|
183
|
-
loader = instaloader.Instaloader(
|
|
184
|
-
quiet=True,
|
|
185
|
-
request_timeout=30,
|
|
186
|
-
max_connection_attempts=1,
|
|
187
|
-
download_pictures=False,
|
|
188
|
-
download_videos=False,
|
|
189
|
-
download_video_thumbnails=False,
|
|
190
|
-
save_metadata=False,
|
|
191
|
-
)
|
|
192
|
-
profile = instaloader.Profile.from_username(loader.context, username)
|
|
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
|
-
)
|
|
202
|
-
return {
|
|
203
|
-
'id': str(getattr(profile, 'userid', '') or ''),
|
|
204
|
-
'username': str(getattr(profile, 'username', '') or username),
|
|
205
|
-
'full_name': str(getattr(profile, 'full_name', '') or ''),
|
|
206
|
-
'biography': str(getattr(profile, 'biography', '') or ''),
|
|
207
|
-
'follower_count': getattr(profile, 'follower_count', getattr(profile, 'followers', None)),
|
|
208
|
-
'following_count': getattr(profile, 'following_count', getattr(profile, 'followees', None)),
|
|
209
|
-
'edge_owner_to_timeline_media': {'count': getattr(profile, 'mediacount', None)},
|
|
210
|
-
'country': country,
|
|
211
|
-
'is_private': bool(getattr(profile, 'is_private', False)),
|
|
212
|
-
'_instagram_source': 'instaloader',
|
|
213
|
-
}
|
|
214
|
-
except Exception as exc:
|
|
215
|
-
LOGGER.warning('Fallback instaloader falhou para @%s: %s', username, exc, exc_info=True)
|
|
216
|
-
return None
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
def _fetch_posts_instaloader(username: str, limit: int = 10) -> list[dict[str, Any]]:
|
|
220
|
-
"""Fetch public posts anonymously through Instaloader when Playwright fails."""
|
|
221
|
-
instaloader = _instagram_instaloader()
|
|
222
|
-
if instaloader is None:
|
|
223
|
-
return []
|
|
224
|
-
posts: list[dict[str, Any]] = []
|
|
225
|
-
try:
|
|
226
|
-
loader = instaloader.Instaloader(
|
|
227
|
-
quiet=True,
|
|
228
|
-
request_timeout=30,
|
|
229
|
-
max_connection_attempts=1,
|
|
230
|
-
download_pictures=False,
|
|
231
|
-
download_videos=False,
|
|
232
|
-
download_video_thumbnails=False,
|
|
233
|
-
save_metadata=False,
|
|
234
|
-
)
|
|
235
|
-
profile = instaloader.Profile.from_username(loader.context, username)
|
|
236
|
-
for post in profile.get_posts():
|
|
237
|
-
shortcode = str(getattr(post, 'shortcode', '') or '')
|
|
238
|
-
if not shortcode:
|
|
239
|
-
continue
|
|
240
|
-
image_url = str(getattr(post, 'url', '') or '')
|
|
241
|
-
timestamp = getattr(post, 'date_utc', '') or ''
|
|
242
|
-
if hasattr(timestamp, 'isoformat'):
|
|
243
|
-
timestamp = timestamp.isoformat()
|
|
244
|
-
else:
|
|
245
|
-
timestamp = str(timestamp)
|
|
246
|
-
posts.append({
|
|
247
|
-
'id': str(getattr(post, 'mediaid', '') or shortcode),
|
|
248
|
-
'shortcode': shortcode,
|
|
249
|
-
'url': f'https://www.instagram.com/p/{shortcode}/',
|
|
250
|
-
'image_url': image_url,
|
|
251
|
-
'display_url': image_url,
|
|
252
|
-
'caption': str(getattr(post, 'caption', '') or '').strip(),
|
|
253
|
-
'published_at': timestamp,
|
|
254
|
-
'timestamp': timestamp,
|
|
255
|
-
'is_video': bool(getattr(post, 'is_video', False)),
|
|
256
|
-
})
|
|
257
|
-
if len(posts) >= max(1, int(limit)):
|
|
258
|
-
break
|
|
259
|
-
LOGGER.info('Fallback instaloader carregou %s posts para @%s', len(posts), username)
|
|
260
|
-
except Exception as exc:
|
|
261
|
-
LOGGER.warning('Fallback instaloader falhou nos posts de @%s: %s', username, exc, exc_info=True)
|
|
262
|
-
return posts
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
def fetch_profile_instaloader(username: str) -> dict[str, Any] | None:
|
|
266
|
-
return _fetch_profile_instaloader(username)
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
def fetch_posts_instaloader(username: str, limit: int = 10) -> list[dict[str, Any]]:
|
|
270
|
-
return _fetch_posts_instaloader(username, limit)
|
|
271
|
-
|
|
272
|
-
|
|
273
144
|
@dataclass
|
|
274
145
|
class IntegrationResult:
|
|
275
146
|
ok: bool
|
|
@@ -429,117 +300,103 @@ def _profile_users_from_document(document: str) -> list[Mapping[str, Any]]:
|
|
|
429
300
|
if isinstance(candidate, Mapping) and any(key in candidate for key in ('biography', 'edge_follow', 'edge_followed_by', 'username')):
|
|
430
301
|
users.append(candidate)
|
|
431
302
|
unique: list[Mapping[str, Any]] = []
|
|
432
|
-
seen: set[
|
|
303
|
+
seen: set[str] = set()
|
|
433
304
|
for user in users:
|
|
434
|
-
marker = id(user)
|
|
305
|
+
marker = str(user.get('id') or user.get('pk') or user.get('username') or id(user))
|
|
435
306
|
if marker not in seen:
|
|
436
307
|
seen.add(marker)
|
|
437
308
|
unique.append(user)
|
|
438
309
|
return unique
|
|
439
310
|
|
|
440
311
|
|
|
441
|
-
def
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
312
|
+
def _extract_posts_from_documents(documents: list[Any], limit: int) -> list[dict[str, Any]]:
|
|
313
|
+
posts: list[dict[str, Any]] = []
|
|
314
|
+
seen: set[str] = set()
|
|
315
|
+
for document in documents:
|
|
316
|
+
for node in _walk_json(document):
|
|
317
|
+
if not isinstance(node, Mapping):
|
|
318
|
+
continue
|
|
319
|
+
post = _post_from_node(dict(node))
|
|
320
|
+
if not post or post['id'] in seen:
|
|
321
|
+
continue
|
|
322
|
+
seen.add(post['id'])
|
|
323
|
+
posts.append(post)
|
|
324
|
+
if len(posts) >= max(1, int(limit)):
|
|
325
|
+
return posts
|
|
326
|
+
return posts
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _extract_profile_from_html(html_content: str) -> dict[str, Any]:
|
|
330
|
+
"""Extract the public profile and available posts from rendered Instagram HTML."""
|
|
331
|
+
html = str(html_content or '')
|
|
332
|
+
documents = _shared_data_documents(html) + _embedded_json_documents(html)
|
|
333
|
+
users = _profile_users_from_document(html)
|
|
334
|
+
user = max(users, key=_profile_quality, default={})
|
|
335
|
+
posts = _extract_posts_from_documents(documents, limit=10)
|
|
336
|
+
result = {'user': dict(user) if isinstance(user, Mapping) else {}, 'posts': posts}
|
|
337
|
+
LOGGER.info('Instagram HTML extraction profile=%s posts=%s', bool(result['user']), len(posts))
|
|
338
|
+
print(f"Playwright usado no Windows, dados extraídos com {'sucesso' if result['user'] or posts else 'falha'}")
|
|
339
|
+
return result
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _extract_posts_from_html(html_content: str, limit: int = 10) -> list[dict[str, Any]]:
|
|
343
|
+
"""Extract public posts from _sharedData and JSON scripts in rendered HTML."""
|
|
344
|
+
html = str(html_content or '')
|
|
345
|
+
documents = _shared_data_documents(html) + _embedded_json_documents(html)
|
|
346
|
+
posts = _extract_posts_from_documents(documents, limit=max(1, int(limit)))
|
|
347
|
+
LOGGER.info('Instagram HTML post extraction posts=%s limit=%s', len(posts), limit)
|
|
348
|
+
print(f"Playwright usado no Windows, posts extraídos: {len(posts)}")
|
|
349
|
+
return posts
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _fetch_profile_with_playwright(username: str) -> dict[str, Any]:
|
|
353
|
+
"""Load a public profile page and extract only its rendered HTML."""
|
|
354
|
+
url = f'https://www.instagram.com/{username}/'
|
|
446
355
|
try:
|
|
447
356
|
from playwright.sync_api import TimeoutError as PlaywrightTimeoutError, sync_playwright
|
|
448
357
|
with sync_playwright() as playwright:
|
|
449
358
|
browser = playwright.chromium.launch(headless=_instagram_playwright_headless())
|
|
450
359
|
try:
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
context_kwargs['storage_state'] = str(state_path)
|
|
456
|
-
context = browser.new_context(**context_kwargs)
|
|
457
|
-
if cookies:
|
|
458
|
-
context.add_cookies([{'name': name, 'value': value, 'domain': '.instagram.com', 'path': '/'} for name, value in cookies.items()])
|
|
360
|
+
context = browser.new_context(
|
|
361
|
+
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/131.0 Safari/537.36',
|
|
362
|
+
extra_http_headers={'Accept-Language': 'pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7'},
|
|
363
|
+
)
|
|
459
364
|
page = context.new_page()
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
return
|
|
466
|
-
status = getattr(response, 'status', None)
|
|
467
|
-
LOGGER.info('Instagram Playwright API response URL=%s status=%s', response_url, status)
|
|
468
|
-
try:
|
|
469
|
-
payload = response.json()
|
|
470
|
-
user = _extract_profile_user(payload)
|
|
471
|
-
if isinstance(user, Mapping):
|
|
472
|
-
captured_user = _merge_profile(captured_user, user)
|
|
473
|
-
LOGGER.info('Instagram Playwright perfil capturado URL=%s status=%s quality=%s', response_url, status, _profile_quality(captured_user))
|
|
474
|
-
except (ValueError, TypeError, AttributeError) as exc:
|
|
475
|
-
LOGGER.warning('Não foi possível interpretar resposta Instagram URL=%s status=%s: %s', response_url, status, exc)
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
page.on('response', capture_profile_response)
|
|
479
|
-
try:
|
|
480
|
-
page.goto('https://www.instagram.com/', wait_until='domcontentloaded', timeout=30000)
|
|
481
|
-
state_path.parent.mkdir(parents=True, exist_ok=True)
|
|
482
|
-
context.storage_state(path=str(state_path))
|
|
483
|
-
LOGGER.debug('Instagram Playwright página inicial carregada; storage_state salvo em %s.', state_path)
|
|
484
|
-
except Exception as exc:
|
|
485
|
-
LOGGER.debug('Instagram Playwright não carregou a página inicial: %s', exc)
|
|
486
|
-
url = f'https://www.instagram.com/{username}/'
|
|
487
|
-
response = None
|
|
488
|
-
status = None
|
|
489
|
-
try:
|
|
490
|
-
response = page.goto(url, wait_until='domcontentloaded', timeout=30000)
|
|
491
|
-
status = response.status if response else None
|
|
492
|
-
except Exception as exc:
|
|
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
|
|
365
|
+
response = page.goto(url, wait_until='domcontentloaded', timeout=30000)
|
|
366
|
+
status = response.status if response else None
|
|
367
|
+
LOGGER.info('Instagram Playwright HTML profile URL=%s status=%s', url, status)
|
|
368
|
+
print(f'Playwright usado no Windows para o perfil @{username}')
|
|
369
|
+
page.wait_for_timeout(5000)
|
|
499
370
|
try:
|
|
500
371
|
page.wait_for_load_state('networkidle', timeout=10000)
|
|
501
372
|
except (PlaywrightTimeoutError, TimeoutError):
|
|
502
|
-
LOGGER.debug('Instagram Playwright networkidle excedido; usando
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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)
|
|
373
|
+
LOGGER.debug('Instagram Playwright networkidle excedido; usando HTML já carregado.')
|
|
374
|
+
html_content = page.content()
|
|
375
|
+
_save_instagram_debug_html(username, html_content)
|
|
376
|
+
extracted = _extract_profile_from_html(html_content)
|
|
377
|
+
user = extracted.get('user') if isinstance(extracted, Mapping) else {}
|
|
378
|
+
if isinstance(user, Mapping) and user:
|
|
379
|
+
LOGGER.info('Instagram Playwright HTML profile extracted URL=%s status=%s', url, status)
|
|
380
|
+
return dict(user)
|
|
381
|
+
return {}
|
|
517
382
|
finally:
|
|
518
383
|
browser.close()
|
|
519
384
|
except Exception as exc:
|
|
520
|
-
LOGGER.warning('Falha no Playwright do perfil Instagram: %s', exc, exc_info=True)
|
|
385
|
+
LOGGER.warning('Falha no Playwright HTML do perfil Instagram URL=%s: %s', url, exc, exc_info=True)
|
|
386
|
+
print(f'Playwright falhou no Windows para o perfil @{username}: {exc}')
|
|
521
387
|
_streamlit_message('error', f'Playwright não conseguiu consultar o perfil Instagram: {exc}', 'profile_playwright_error')
|
|
522
|
-
|
|
388
|
+
return {}
|
|
523
389
|
|
|
524
390
|
|
|
525
391
|
def _fetch_web_profile_user(username: str) -> dict[str, Any] | None:
|
|
526
392
|
_ensure_windows_playwright()
|
|
527
|
-
cookies = _instagram_cookies()
|
|
528
393
|
headers = _instagram_headers()
|
|
529
394
|
headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/131.0 Safari/537.36'
|
|
530
395
|
if platform.system() == 'Windows':
|
|
531
|
-
LOGGER.debug('Windows detectado: perfil Instagram exclusivamente via Playwright.')
|
|
532
|
-
|
|
533
|
-
if playwright_user:
|
|
534
|
-
return playwright_user
|
|
535
|
-
LOGGER.warning('Fallback para instaloader activado no perfil @%s.', username)
|
|
536
|
-
fallback_user = fetch_profile_instaloader(username)
|
|
537
|
-
if fallback_user:
|
|
538
|
-
_streamlit_message('info', 'Dados obtidos via fallback (instaloader). O país pode não estar disponível.', 'profile_instaloader_fallback')
|
|
539
|
-
else:
|
|
540
|
-
_streamlit_message('error', 'Não foi possível obter os dados do Instagram. Verifique sua conexão ou tente novamente mais tarde.', 'profile_instagram_failed')
|
|
541
|
-
return fallback_user
|
|
396
|
+
LOGGER.debug('Windows detectado: perfil Instagram exclusivamente via HTML Playwright sem cookies manuais.')
|
|
397
|
+
return _fetch_profile_with_playwright(username)
|
|
542
398
|
|
|
399
|
+
cookies = _instagram_cookies()
|
|
543
400
|
endpoints = (
|
|
544
401
|
f'https://www.instagram.com/api/v1/users/web_profile_info/?username={username}',
|
|
545
402
|
f'https://i.instagram.com/api/v1/users/web_profile_info/?username={username}',
|
|
@@ -599,7 +456,7 @@ def _fetch_web_profile_user(username: str) -> dict[str, Any] | None:
|
|
|
599
456
|
LOGGER.warning('curl não está disponível no PATH; a tentar Playwright.')
|
|
600
457
|
_streamlit_message('warning', 'curl não foi encontrado no PATH do Windows; será tentado o fallback Playwright.', 'curl_missing')
|
|
601
458
|
|
|
602
|
-
return _fetch_profile_with_playwright(username
|
|
459
|
+
return _fetch_profile_with_playwright(username) or best_user
|
|
603
460
|
|
|
604
461
|
|
|
605
462
|
def _country_from_bloks(value: Any) -> str:
|
|
@@ -708,7 +565,9 @@ def _profile_data_from_api(user: Mapping[str, Any], reference: Mapping[str, str]
|
|
|
708
565
|
media = media if media is not None else _profile_metric(user, 'posts', 'post_count', 'media_count')
|
|
709
566
|
biography = str(user.get('biography') or user.get('bio') or user.get('description') or '').strip()
|
|
710
567
|
direct_country = _country_value(user.get('country') or user.get('country_code'))
|
|
711
|
-
country = direct_country or extract_public_instagram_country(user)
|
|
568
|
+
country = direct_country or extract_public_instagram_country(user)
|
|
569
|
+
if platform.system() != 'Windows':
|
|
570
|
+
country = country or _fetch_instagram_about_country(user.get('id') or user.get('pk'))
|
|
712
571
|
country = country or _country_from_bio_fallback(biography)
|
|
713
572
|
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 '—')
|
|
714
573
|
return {
|
|
@@ -800,8 +659,6 @@ def fetch_public_instagram_profile(source: str) -> IntegrationResult:
|
|
|
800
659
|
api_user = _fetch_web_profile_user(reference['username'])
|
|
801
660
|
if api_user:
|
|
802
661
|
data = _profile_data_from_api(api_user, reference)
|
|
803
|
-
if api_user.get('_instagram_source') == 'instaloader':
|
|
804
|
-
return IntegrationResult(True, 'Perfil encontrado via fallback instaloader. O país pode não estar disponível.', data)
|
|
805
662
|
return IntegrationResult(True, 'Perfil Instagram encontrado publicamente.', data)
|
|
806
663
|
if platform.system() == 'Windows':
|
|
807
664
|
return IntegrationResult(False, 'Não foi possível obter os dados do Instagram. Verifique sua conexão ou tente novamente mais tarde.', reference)
|
|
@@ -898,66 +755,40 @@ def _post_from_node(node: dict[str, Any]) -> dict[str, Any] | None:
|
|
|
898
755
|
}
|
|
899
756
|
|
|
900
757
|
|
|
901
|
-
def _fetch_posts_with_playwright(reference: Mapping[str, str], limit: int
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
seen: set[str] = set()
|
|
905
|
-
cookies = cookies or _instagram_cookies()
|
|
758
|
+
def _fetch_posts_with_playwright(reference: Mapping[str, str], limit: int) -> list[dict[str, Any]]:
|
|
759
|
+
"""Load a public profile page and extract posts only from its rendered HTML."""
|
|
760
|
+
url = str(reference.get('url') or '')
|
|
906
761
|
try:
|
|
907
762
|
from playwright.sync_api import TimeoutError as PlaywrightTimeoutError, sync_playwright
|
|
908
763
|
with sync_playwright() as playwright:
|
|
909
764
|
browser = playwright.chromium.launch(headless=_instagram_playwright_headless())
|
|
910
765
|
try:
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
context_kwargs['storage_state'] = str(state_path)
|
|
916
|
-
context = browser.new_context(**context_kwargs)
|
|
917
|
-
if cookies:
|
|
918
|
-
context.add_cookies([{'name': name, 'value': value, 'domain': '.instagram.com', 'path': '/'} for name, value in cookies.items()])
|
|
766
|
+
context = browser.new_context(
|
|
767
|
+
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/131.0 Safari/537.36',
|
|
768
|
+
extra_http_headers={'Accept-Language': 'pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7'},
|
|
769
|
+
)
|
|
919
770
|
page = context.new_page()
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
return
|
|
925
|
-
LOGGER.info('Instagram Playwright posts response URL=%s status=%s', response_url, getattr(response, 'status', None))
|
|
926
|
-
try:
|
|
927
|
-
payload = response.json()
|
|
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))
|
|
931
|
-
if post and post['id'] not in seen:
|
|
932
|
-
seen.add(post['id'])
|
|
933
|
-
posts.append(post)
|
|
934
|
-
except (ValueError, TypeError, AttributeError) as exc:
|
|
935
|
-
LOGGER.debug('Não foi possível interpretar posts web_profile_info: %s', exc)
|
|
936
|
-
|
|
937
|
-
page.on('response', capture_profile_posts)
|
|
938
|
-
try:
|
|
939
|
-
page.goto('https://www.instagram.com/', wait_until='domcontentloaded', timeout=30000)
|
|
940
|
-
state_path.parent.mkdir(parents=True, exist_ok=True)
|
|
941
|
-
context.storage_state(path=str(state_path))
|
|
942
|
-
except Exception as exc:
|
|
943
|
-
LOGGER.debug('Instagram Playwright não carregou a página inicial para posts: %s', exc)
|
|
944
|
-
response = page.goto(reference['url'], wait_until='domcontentloaded', timeout=30000)
|
|
945
|
-
LOGGER.debug('Instagram Playwright posts URL=%s status=%s', reference['url'], response.status if response else None)
|
|
946
|
-
if response is None or response.status >= 400:
|
|
947
|
-
return []
|
|
771
|
+
response = page.goto(url, wait_until='domcontentloaded', timeout=30000)
|
|
772
|
+
status = response.status if response else None
|
|
773
|
+
LOGGER.info('Instagram Playwright HTML posts URL=%s status=%s', url, status)
|
|
774
|
+
print(f'Playwright usado no Windows para os posts de @{reference.get("username", "")}'.strip())
|
|
948
775
|
page.wait_for_timeout(5000)
|
|
949
776
|
try:
|
|
950
777
|
page.wait_for_load_state('networkidle', timeout=10000)
|
|
951
778
|
except (PlaywrightTimeoutError, TimeoutError):
|
|
952
|
-
LOGGER.debug('Instagram Playwright networkidle excedido; usando
|
|
953
|
-
|
|
954
|
-
|
|
779
|
+
LOGGER.debug('Instagram Playwright networkidle excedido; usando HTML já carregado para posts.')
|
|
780
|
+
html_content = page.content()
|
|
781
|
+
_save_instagram_debug_html(reference.get('username', 'instagram'), html_content)
|
|
782
|
+
if status is not None and status >= 400:
|
|
783
|
+
return []
|
|
784
|
+
return _extract_posts_from_html(html_content, limit)
|
|
955
785
|
finally:
|
|
956
786
|
browser.close()
|
|
957
787
|
except Exception as exc:
|
|
958
|
-
LOGGER.warning('Falha no Playwright dos posts Instagram: %s', exc, exc_info=True)
|
|
788
|
+
LOGGER.warning('Falha no Playwright HTML dos posts Instagram URL=%s: %s', url, exc, exc_info=True)
|
|
789
|
+
print(f'Playwright falhou no Windows para os posts: {exc}')
|
|
959
790
|
_streamlit_message('error', f'Playwright não conseguiu consultar os posts Instagram: {exc}', 'posts_playwright_error')
|
|
960
|
-
|
|
791
|
+
return []
|
|
961
792
|
|
|
962
793
|
|
|
963
794
|
def fetch_public_instagram_posts(source: str, limit: int = 10) -> IntegrationResult:
|
|
@@ -968,15 +799,10 @@ def fetch_public_instagram_posts(source: str, limit: int = 10) -> IntegrationRes
|
|
|
968
799
|
except ValueError as exc:
|
|
969
800
|
return IntegrationResult(False, str(exc), {})
|
|
970
801
|
if platform.system() == 'Windows':
|
|
971
|
-
LOGGER.debug('Windows detectado: posts Instagram exclusivamente via Playwright.')
|
|
972
|
-
posts = _fetch_posts_with_playwright(reference, limit
|
|
802
|
+
LOGGER.debug('Windows detectado: posts Instagram exclusivamente via HTML Playwright.')
|
|
803
|
+
posts = _fetch_posts_with_playwright(reference, limit)
|
|
973
804
|
if not posts:
|
|
974
|
-
|
|
975
|
-
posts = fetch_posts_instaloader(reference['username'], limit)
|
|
976
|
-
if posts:
|
|
977
|
-
_streamlit_message('info', 'Dados obtidos via fallback (instaloader). O país pode não estar disponível.', 'posts_instaloader_fallback')
|
|
978
|
-
else:
|
|
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')
|
|
805
|
+
_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')
|
|
980
806
|
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))]})
|
|
981
807
|
api_user = _fetch_web_profile_user(reference['username'])
|
|
982
808
|
if api_user:
|
|
@@ -1012,9 +838,9 @@ def fetch_public_instagram_posts(source: str, limit: int = 10) -> IntegrationRes
|
|
|
1012
838
|
if len(posts) >= max(1, int(limit)):
|
|
1013
839
|
break
|
|
1014
840
|
if not posts:
|
|
1015
|
-
posts = _fetch_posts_with_playwright(reference, limit
|
|
841
|
+
posts = _fetch_posts_with_playwright(reference, limit)
|
|
1016
842
|
LOGGER.debug('Instagram posts extracted=%s limit=%s', len(posts), limit)
|
|
1017
843
|
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))]})
|
|
1018
844
|
|
|
1019
845
|
|
|
1020
|
-
__all__ = ['IntegrationResult', 'extract_public_instagram_country', '
|
|
846
|
+
__all__ = ['IntegrationResult', 'extract_public_instagram_country', 'fetch_public_instagram_posts', 'fetch_public_instagram_profile', 'normalize_instagram_bio', 'normalize_instagram_metric', 'normalize_instagram_reference']
|
package/package.json
CHANGED
package/requirements.txt
CHANGED